docs: Update README and documentation for lxs multi-tool across Linux and Windows platforms.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
<#
|
||||
LXS - Disk Cleanup (Windows)
|
||||
Description: Reclaim disk space (temp files, Windows Update cache, recycle
|
||||
bin, Prefetch, error reports, WinSxS component store).
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
|
||||
Usage: cleanup.ps1 [-Yes] [-NoUpdateCache] [-NoRecycleBin] [-NoPrefetch]
|
||||
[-NoComponentCleanup] [-Help]
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_cleanup.log'
|
||||
|
||||
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
|
||||
'-Yes', '-y', '-NoUpdateCache', '-NoRecycleBin', '-NoPrefetch',
|
||||
'-NoComponentCleanup', '-Help', '-h'
|
||||
)
|
||||
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
|
||||
$Yes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
|
||||
$NoUpdateCache = Test-LxsFlag $LxsOpts @('-NoUpdateCache')
|
||||
$NoRecycleBin = Test-LxsFlag $LxsOpts @('-NoRecycleBin')
|
||||
$NoPrefetch = Test-LxsFlag $LxsOpts @('-NoPrefetch')
|
||||
$NoComponentCleanup = Test-LxsFlag $LxsOpts @('-NoComponentCleanup')
|
||||
$Help = Test-LxsFlag $LxsOpts @('-Help', '-h')
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @'
|
||||
Usage: cleanup.ps1 [options]
|
||||
|
||||
Options:
|
||||
-Yes Skip the confirmation prompt
|
||||
-NoUpdateCache Skip the Windows Update download cache
|
||||
-NoRecycleBin Skip emptying the recycle bin
|
||||
-NoPrefetch Skip the Prefetch folder
|
||||
-NoComponentCleanup Skip DISM /StartComponentCleanup (the slow one)
|
||||
-Help Show this help
|
||||
'@
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Get-LxsFreeBytes {
|
||||
try { return (New-Object System.IO.DriveInfo "$env:SystemDrive\").AvailableFreeSpace } catch { return 0 }
|
||||
}
|
||||
|
||||
function Format-LxsSize {
|
||||
param([double]$Bytes)
|
||||
if ($Bytes -ge 1GB) { return ('{0:N2} GB' -f ($Bytes / 1GB)) }
|
||||
if ($Bytes -ge 1MB) { return ('{0:N1} MB' -f ($Bytes / 1MB)) }
|
||||
return ('{0:N0} KB' -f ($Bytes / 1KB))
|
||||
}
|
||||
|
||||
function Get-LxsFolderSize {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) { return 0 }
|
||||
try {
|
||||
$sum = (Get-ChildItem -Path $Path -Recurse -Force -File -ErrorAction SilentlyContinue |
|
||||
Measure-Object -Property Length -Sum).Sum
|
||||
if ($null -eq $sum) { return 0 }
|
||||
return $sum
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
# Delete the *contents* of a folder, never the folder itself. Files locked by a
|
||||
# running process are skipped silently — that is expected on a live system.
|
||||
function Clear-LxsFolderContents {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string]$Label
|
||||
)
|
||||
if (-not (Test-Path $Path)) {
|
||||
Write-Host "$($script:Gray) $Label — not present, skipped$($script:NC)"
|
||||
return 0
|
||||
}
|
||||
$before = Get-LxsFolderSize -Path $Path
|
||||
$skipped = 0
|
||||
Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try {
|
||||
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction Stop
|
||||
} catch {
|
||||
$skipped++
|
||||
}
|
||||
}
|
||||
$after = Get-LxsFolderSize -Path $Path
|
||||
$freed = [math]::Max(0, $before - $after)
|
||||
$suffix = if ($skipped -gt 0) { " ($skipped item(s) in use, skipped)" } else { '' }
|
||||
Write-LxsOk "$Label — $(Format-LxsSize $freed) reclaimed$suffix"
|
||||
return $freed
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Plan
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
|
||||
$isAdmin = Test-LxsAdmin
|
||||
$doUpdateCache = (-not $NoUpdateCache) -and $isAdmin
|
||||
$doPrefetch = (-not $NoPrefetch) -and $isAdmin
|
||||
$doComponent = (-not $NoComponentCleanup) -and $isAdmin
|
||||
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISK CLEANUP'
|
||||
Write-Host ''
|
||||
Write-Host 'The following locations will be cleaned:'
|
||||
Write-Host ' - User temp folder'
|
||||
if ($isAdmin) { Write-Host ' - System temp folder (C:\Windows\Temp)' }
|
||||
if (-not $NoRecycleBin) { Write-Host ' - Recycle bin (all drives)' }
|
||||
if ($doUpdateCache) { Write-Host ' - Windows Update download cache' }
|
||||
if ($doPrefetch) { Write-Host ' - Prefetch folder' }
|
||||
if ($isAdmin) { Write-Host ' - Windows Error Reporting queue and Delivery Optimization cache' }
|
||||
if ($doComponent) { Write-Host ' - WinSxS component store (DISM /StartComponentCleanup) - slow' }
|
||||
Write-Host ''
|
||||
|
||||
if (-not $isAdmin) {
|
||||
Write-LxsWarn 'Not running as administrator: system-wide locations are skipped.'
|
||||
Write-Host "$($script:Gray) Re-run from an elevated terminal to clean them.$($script:NC)"
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Proceed with the cleanup?' -DefaultYes -AssumeYes:$Yes)) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Run
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$freeBefore = Get-LxsFreeBytes
|
||||
Write-Host ''
|
||||
Write-LxsInfo "Free space before: $(Format-LxsSize $freeBefore)"
|
||||
Write-Host ''
|
||||
|
||||
Clear-LxsFolderContents -Path (Get-LxsTempDir) -Label 'User temp' | Out-Null
|
||||
|
||||
if ($isAdmin) {
|
||||
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Temp') -Label 'System temp' | Out-Null
|
||||
Clear-LxsFolderContents -Path (Join-Path $env:ProgramData 'Microsoft\Windows\WER\ReportQueue') -Label 'Error reports' | Out-Null
|
||||
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\DeliveryOptimization') -Label 'Delivery Optimization' | Out-Null
|
||||
}
|
||||
|
||||
if ($doPrefetch) {
|
||||
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'Prefetch') -Label 'Prefetch' | Out-Null
|
||||
}
|
||||
|
||||
if (-not $NoRecycleBin) {
|
||||
try {
|
||||
Clear-RecycleBin -Force -ErrorAction Stop
|
||||
Write-LxsOk 'Recycle bin emptied'
|
||||
} catch {
|
||||
# Clear-RecycleBin throws when the bin is already empty.
|
||||
Write-Host "$($script:Gray) Recycle bin — already empty or not accessible$($script:NC)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($doUpdateCache) {
|
||||
# The download cache can only be cleared with the update services stopped;
|
||||
# they are restarted afterwards whether or not the deletion succeeded.
|
||||
$services = @('wuauserv', 'bits')
|
||||
$stopped = @()
|
||||
foreach ($svc in $services) {
|
||||
try {
|
||||
$s = Get-Service -Name $svc -ErrorAction Stop
|
||||
if ($s.Status -eq 'Running') {
|
||||
Stop-Service -Name $svc -Force -ErrorAction Stop
|
||||
$stopped += $svc
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "Could not stop $svc — the update cache may be partially locked."
|
||||
}
|
||||
}
|
||||
try {
|
||||
Clear-LxsFolderContents -Path (Join-Path $env:SystemRoot 'SoftwareDistribution\Download') -Label 'Windows Update cache' | Out-Null
|
||||
} finally {
|
||||
foreach ($svc in $stopped) {
|
||||
try { Start-Service -Name $svc -ErrorAction Stop } catch { Write-LxsWarn "Failed to restart $svc — start it manually." }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($doComponent) {
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Running DISM component cleanup (several minutes, no output until done)...'
|
||||
$ok = Invoke-LxsSpinner -Message 'DISM /StartComponentCleanup' -FilePath 'dism.exe' `
|
||||
-ArgumentList @('/Online', '/Cleanup-Image', '/StartComponentCleanup')
|
||||
if (-not $ok) { Write-LxsWarn 'Component cleanup did not complete; see the log above.' }
|
||||
}
|
||||
|
||||
# Windows.old is reported, never auto-deleted: removing it forfeits the
|
||||
# ability to roll back a feature update.
|
||||
$windowsOld = Join-Path $env:SystemDrive 'Windows.old'
|
||||
if (Test-Path $windowsOld) {
|
||||
Write-Host ''
|
||||
Write-LxsWarn "$windowsOld exists ($(Format-LxsSize (Get-LxsFolderSize -Path $windowsOld)))."
|
||||
Write-Host "$($script:Gray) It holds your previous Windows install. Removing it blocks rollback;$($script:NC)"
|
||||
Write-Host "$($script:Gray) use Settings > System > Storage > Temporary files if you want it gone.$($script:NC)"
|
||||
}
|
||||
|
||||
$freeAfter = Get-LxsFreeBytes
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
Write-LxsOk "Free space after: $(Format-LxsSize $freeAfter)"
|
||||
Write-LxsOk "Reclaimed: $(Format-LxsSize ([math]::Max(0, $freeAfter - $freeBefore)))"
|
||||
Write-Host ''
|
||||
exit 0
|
||||
@@ -0,0 +1,483 @@
|
||||
<#
|
||||
LXS - Debloat and privacy (Windows)
|
||||
Description: Remove preinstalled apps you pick, and turn off telemetry,
|
||||
Cortana, ads, tracking tasks, Copilot and Recall.
|
||||
|
||||
Safety rules this script follows:
|
||||
- a restore point is created before the first change of a session
|
||||
- no app is removed unless you explicitly select it
|
||||
- the registry/service/task tweaks can be undone from the menu (option 8)
|
||||
- apps you remove come back only by reinstalling them from the Store
|
||||
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_debloat.log'
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
|
||||
|
||||
$script:LxsCheckpointDone = $false
|
||||
|
||||
# One restore point per session, created lazily before the first real change.
|
||||
function Assert-LxsCheckpoint {
|
||||
if ($script:LxsCheckpointDone) { return $true }
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Creating a restore point before changing anything...'
|
||||
$ok = New-LxsRestorePoint -Description 'LXS before debloat'
|
||||
if (-not $ok) {
|
||||
Write-Host ''
|
||||
Write-LxsWarn 'The restore point could not be created.'
|
||||
if (-not (Confirm-LxsAction -Question 'Continue anyway, without a rollback point?')) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
$script:LxsCheckpointDone = $true
|
||||
return $true
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Preinstalled apps
|
||||
#
|
||||
# Only packages on this list are ever offered. Anything not listed here —
|
||||
# including the Store, App Installer (winget), Terminal, Defender UI, the
|
||||
# VCLibs/.NET runtime frameworks and every driver package — is left alone,
|
||||
# because removing those breaks Windows or LXS itself.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$LxsRemovableApps = @(
|
||||
@{ Pattern = 'Microsoft.3DBuilder'; Label = '3D Builder' }
|
||||
@{ Pattern = 'Microsoft.549981C3F5F10'; Label = 'Cortana' }
|
||||
@{ Pattern = 'Microsoft.BingFinance'; Label = 'Bing Finance' }
|
||||
@{ Pattern = 'Microsoft.BingNews'; Label = 'Bing News' }
|
||||
@{ Pattern = 'Microsoft.BingSports'; Label = 'Bing Sports' }
|
||||
@{ Pattern = 'Microsoft.BingWeather'; Label = 'Weather' }
|
||||
@{ Pattern = 'Microsoft.BingSearch'; Label = 'Web Search (Bing)' }
|
||||
@{ Pattern = 'Microsoft.GetHelp'; Label = 'Get Help' }
|
||||
@{ Pattern = 'Microsoft.Getstarted'; Label = 'Tips / Get Started' }
|
||||
@{ Pattern = 'Microsoft.Messaging'; Label = 'Messaging' }
|
||||
@{ Pattern = 'Microsoft.Microsoft3DViewer'; Label = '3D Viewer' }
|
||||
@{ Pattern = 'Microsoft.MicrosoftJournal'; Label = 'Journal' }
|
||||
@{ Pattern = 'Microsoft.MicrosoftOfficeHub'; Label = 'Office Hub' }
|
||||
@{ Pattern = 'Microsoft.MicrosoftSolitaireCollection'; Label = 'Solitaire Collection' }
|
||||
@{ Pattern = 'Microsoft.MixedReality.Portal'; Label = 'Mixed Reality Portal' }
|
||||
@{ Pattern = 'Microsoft.NetworkSpeedTest'; Label = 'Network Speed Test' }
|
||||
@{ Pattern = 'Microsoft.News'; Label = 'News' }
|
||||
@{ Pattern = 'Microsoft.Office.OneNote'; Label = 'OneNote (Store version)' }
|
||||
@{ Pattern = 'Microsoft.Office.Sway'; Label = 'Sway' }
|
||||
@{ Pattern = 'Microsoft.OneConnect'; Label = 'Mobile Plans' }
|
||||
@{ Pattern = 'Microsoft.People'; Label = 'People' }
|
||||
@{ Pattern = 'Microsoft.PowerAutomateDesktop'; Label = 'Power Automate Desktop' }
|
||||
@{ Pattern = 'Microsoft.Print3D'; Label = 'Print 3D' }
|
||||
@{ Pattern = 'Microsoft.SkypeApp'; Label = 'Skype' }
|
||||
@{ Pattern = 'Microsoft.Todos'; Label = 'Microsoft To Do' }
|
||||
@{ Pattern = 'Microsoft.Wallet'; Label = 'Wallet' }
|
||||
@{ Pattern = 'Microsoft.WindowsAlarms'; Label = 'Alarms & Clock' }
|
||||
@{ Pattern = 'Microsoft.WindowsFeedbackHub'; Label = 'Feedback Hub' }
|
||||
@{ Pattern = 'Microsoft.WindowsMaps'; Label = 'Maps' }
|
||||
@{ Pattern = 'Microsoft.WindowsSoundRecorder'; Label = 'Sound Recorder' }
|
||||
@{ Pattern = 'Microsoft.Xbox.TCUI'; Label = 'Xbox TCUI' }
|
||||
@{ Pattern = 'Microsoft.XboxApp'; Label = 'Xbox Console Companion' }
|
||||
@{ Pattern = 'Microsoft.XboxGameOverlay'; Label = 'Xbox Game Overlay' }
|
||||
@{ Pattern = 'Microsoft.XboxGamingOverlay'; Label = 'Xbox Game Bar' }
|
||||
@{ Pattern = 'Microsoft.XboxIdentityProvider'; Label = 'Xbox Identity Provider' }
|
||||
@{ Pattern = 'Microsoft.XboxSpeechToTextOverlay'; Label = 'Xbox Speech To Text' }
|
||||
@{ Pattern = 'Microsoft.YourPhone'; Label = 'Phone Link' }
|
||||
@{ Pattern = 'Microsoft.ZuneMusic'; Label = 'Media Player / Groove Music' }
|
||||
@{ Pattern = 'Microsoft.ZuneVideo'; Label = 'Movies & TV' }
|
||||
@{ Pattern = 'MicrosoftTeams'; Label = 'Teams (personal / chat)' }
|
||||
@{ Pattern = 'MSTeams'; Label = 'Teams (new client)' }
|
||||
@{ Pattern = 'Clipchamp.Clipchamp'; Label = 'Clipchamp' }
|
||||
@{ Pattern = 'Microsoft.Copilot'; Label = 'Copilot app' }
|
||||
@{ Pattern = 'Microsoft.WindowsFamilySafety'; Label = 'Family Safety' }
|
||||
@{ Pattern = 'SpotifyAB.SpotifyMusic'; Label = 'Spotify (OEM stub)' }
|
||||
@{ Pattern = 'Disney.37853FC22B2CE'; Label = 'Disney+ (OEM stub)' }
|
||||
)
|
||||
|
||||
function Get-LxsInstalledBloat {
|
||||
$found = @()
|
||||
foreach ($entry in $LxsRemovableApps) {
|
||||
try {
|
||||
$pkgs = @(Get-AppxPackage -Name $entry.Pattern -ErrorAction SilentlyContinue)
|
||||
} catch {
|
||||
$pkgs = @()
|
||||
}
|
||||
if ($pkgs.Count -gt 0) {
|
||||
$found += [pscustomobject]@{
|
||||
Label = $entry.Label
|
||||
Pattern = $entry.Pattern
|
||||
Name = $pkgs[0].Name
|
||||
Version = $pkgs[0].Version
|
||||
}
|
||||
}
|
||||
}
|
||||
return $found
|
||||
}
|
||||
|
||||
function Remove-LxsSelectedApps {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'REMOVE PREINSTALLED APPS'
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Scanning installed packages...'
|
||||
$found = @(Get-LxsInstalledBloat)
|
||||
Write-Host ''
|
||||
|
||||
if ($found.Count -eq 0) {
|
||||
Write-LxsOk 'None of the known preinstalled apps are present.'
|
||||
return
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $found.Count; $i++) {
|
||||
Write-Host (" $($script:Cyan)[{0,2}]$($script:NC) $($script:White){1,-32}$($script:NC) $($script:Gray){2}$($script:NC)" -f `
|
||||
($i + 1), $found[$i].Label, $found[$i].Name)
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Enter the numbers to remove, separated by commas (e.g. 1,4,7),$($script:NC)"
|
||||
Write-Host "$($script:Gray)or 'all' for every entry above. Empty cancels — nothing is removed$($script:NC)"
|
||||
Write-Host "$($script:Gray)unless you name it here.$($script:NC)"
|
||||
Write-Host ''
|
||||
$selection = Read-Host -Prompt 'Selection'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($selection)) {
|
||||
Write-LxsInfo 'Cancelled — nothing removed.'
|
||||
return
|
||||
}
|
||||
|
||||
$targets = @()
|
||||
if ($selection.Trim() -ieq 'all') {
|
||||
$targets = $found
|
||||
} else {
|
||||
foreach ($part in ($selection -split ',')) {
|
||||
$part = $part.Trim()
|
||||
if ($part -notmatch '^\d+$') {
|
||||
Write-LxsErr "Not a number: $part"
|
||||
return
|
||||
}
|
||||
$idx = [int]$part - 1
|
||||
if ($idx -lt 0 -or $idx -ge $found.Count) {
|
||||
Write-LxsErr "Out of range: $part"
|
||||
return
|
||||
}
|
||||
$targets += $found[$idx]
|
||||
}
|
||||
}
|
||||
$targets = @($targets | Sort-Object Name -Unique)
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'These will be removed for the current user:'
|
||||
foreach ($t in $targets) { Write-Host " - $($t.Label)" }
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)They can be reinstalled later from the Microsoft Store.$($script:NC)"
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question "Remove $($targets.Count) app(s)?")) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
if (-not (Assert-LxsCheckpoint)) { return }
|
||||
|
||||
$alsoProvisioned = Confirm-LxsAction -Question 'Also remove them from the image, so new user accounts do not get them?'
|
||||
|
||||
Write-Host ''
|
||||
foreach ($t in $targets) {
|
||||
try {
|
||||
Get-AppxPackage -Name $t.Pattern -ErrorAction Stop | Remove-AppxPackage -ErrorAction Stop
|
||||
Write-LxsOk "$($t.Label) removed"
|
||||
} catch {
|
||||
Write-LxsWarn "$($t.Label): $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($alsoProvisioned) {
|
||||
try {
|
||||
$prov = @(Get-AppxProvisionedPackage -Online -ErrorAction Stop |
|
||||
Where-Object { $_.DisplayName -like $t.Pattern })
|
||||
foreach ($p in $prov) {
|
||||
Remove-AppxProvisionedPackage -Online -PackageName $p.PackageName -ErrorAction Stop | Out-Null
|
||||
Write-Host "$($script:Gray) also removed from the image$($script:NC)"
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "$($t.Label): could not remove the provisioned package."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Privacy tweaks
|
||||
#
|
||||
# Every entry carries the value to set and the value to restore, so option 8
|
||||
# can undo exactly what option 2-6 did. Default = $null means "delete the
|
||||
# value", which is what returns a policy key to its Windows default.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
$LxsTweaks = @(
|
||||
# --- telemetry ---
|
||||
@{ Group = 'telemetry'; Label = 'Diagnostic data set to the minimum';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'; Name = 'AllowTelemetry'; Value = 0; Default = $null }
|
||||
@{ Group = 'telemetry'; Label = 'Do not send device names with telemetry';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection'; Name = 'AllowDeviceNameInTelemetry'; Value = 0; Default = $null }
|
||||
@{ Group = 'telemetry'; Label = 'Advertising ID disabled';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\AdvertisingInfo'; Name = 'Enabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'telemetry'; Label = 'App launch tracking disabled';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced'; Name = 'Start_TrackProgs'; Value = 0; Default = 1 }
|
||||
@{ Group = 'telemetry'; Label = 'Feedback requests disabled';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Siuf\Rules'; Name = 'NumberOfSIUFInPeriod'; Value = 0; Default = $null }
|
||||
|
||||
# --- cortana / search ---
|
||||
@{ Group = 'cortana'; Label = 'Cortana disabled';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'AllowCortana'; Value = 0; Default = $null }
|
||||
@{ Group = 'cortana'; Label = 'Web results in Start disabled';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'DisableWebSearch'; Value = 1; Default = $null }
|
||||
@{ Group = 'cortana'; Label = 'Connected web search disabled';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'; Name = 'ConnectedSearchUseWeb'; Value = 0; Default = $null }
|
||||
@{ Group = 'cortana'; Label = 'Search highlights disabled';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\SearchSettings'; Name = 'IsDynamicSearchBoxEnabled'; Value = 0; Default = 1 }
|
||||
|
||||
# --- ads and suggestions ---
|
||||
@{ Group = 'ads'; Label = 'Start menu app suggestions off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SystemPaneSuggestionsEnabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'ads'; Label = 'Silent install of promoted apps off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SilentInstalledAppsEnabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'ads'; Label = 'Lock screen spotlight ads off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'RotatingLockScreenOverlayEnabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'ads'; Label = 'Windows tips and tricks off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-338389Enabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'ads'; Label = 'Suggested content in Settings off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-338393Enabled'; Value = 0; Default = 1 }
|
||||
@{ Group = 'ads'; Label = 'Welcome experience after updates off';
|
||||
Path = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager'; Name = 'SubscribedContent-310093Enabled'; Value = 0; Default = 1 }
|
||||
|
||||
# --- copilot / recall ---
|
||||
@{ Group = 'ai'; Label = 'Windows Copilot turned off';
|
||||
Path = 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot'; Name = 'TurnOffWindowsCopilot'; Value = 1; Default = $null }
|
||||
@{ Group = 'ai'; Label = 'Windows Copilot turned off (machine-wide)';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsCopilot'; Name = 'TurnOffWindowsCopilot'; Value = 1; Default = $null }
|
||||
@{ Group = 'ai'; Label = 'Recall snapshots disabled';
|
||||
Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI'; Name = 'DisableAIDataAnalysis'; Value = 1; Default = $null }
|
||||
@{ Group = 'ai'; Label = 'Recall snapshots disabled (per user)';
|
||||
Path = 'HKCU:\SOFTWARE\Policies\Microsoft\Windows\WindowsAI'; Name = 'DisableAIDataAnalysis'; Value = 1; Default = $null }
|
||||
)
|
||||
|
||||
$LxsTrackingTasks = @(
|
||||
'\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraisal',
|
||||
'\Microsoft\Windows\Application Experience\ProgramDataUpdater',
|
||||
'\Microsoft\Windows\Application Experience\StartupAppTask',
|
||||
'\Microsoft\Windows\Customer Experience Improvement Program\Consolidator',
|
||||
'\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip',
|
||||
'\Microsoft\Windows\Autochk\Proxy',
|
||||
'\Microsoft\Windows\Feedback\Siuf\DmClient',
|
||||
'\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload'
|
||||
)
|
||||
|
||||
$LxsTrackingServices = @(
|
||||
@{ Name = 'DiagTrack'; Label = 'Connected User Experiences and Telemetry'; Default = 'Automatic' },
|
||||
@{ Name = 'dmwappushservice'; Label = 'WAP Push Message Routing'; Default = 'Manual' }
|
||||
)
|
||||
|
||||
function Invoke-LxsTweakGroup {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Group,
|
||||
[Parameter(Mandatory = $true)][string]$Title
|
||||
)
|
||||
$entries = @($LxsTweaks | Where-Object { $_.Group -eq $Group })
|
||||
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title $Title
|
||||
Write-Host ''
|
||||
Write-Host 'The following settings will be changed:'
|
||||
foreach ($e in $entries) { Write-Host " - $($e.Label)" }
|
||||
if ($Group -eq 'telemetry') {
|
||||
Write-Host ' - Telemetry services disabled (DiagTrack, dmwappushservice)'
|
||||
Write-Host ' - Tracking scheduled tasks disabled'
|
||||
}
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)All of this is reversible from menu option 8.$($script:NC)"
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Apply?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
if (-not (Assert-LxsCheckpoint)) { return }
|
||||
|
||||
Write-Host ''
|
||||
foreach ($e in $entries) {
|
||||
if (Set-LxsRegistryValue -Path $e.Path -Name $e.Name -Value $e.Value) {
|
||||
Write-LxsOk $e.Label
|
||||
}
|
||||
}
|
||||
|
||||
if ($Group -ne 'telemetry') { return }
|
||||
|
||||
Write-Host ''
|
||||
foreach ($svc in $LxsTrackingServices) {
|
||||
try {
|
||||
$s = Get-Service -Name $svc.Name -ErrorAction Stop
|
||||
if ($s.Status -eq 'Running') { Stop-Service -Name $svc.Name -Force -ErrorAction SilentlyContinue }
|
||||
Set-Service -Name $svc.Name -StartupType Disabled -ErrorAction Stop
|
||||
Write-LxsOk "$($svc.Label) service disabled"
|
||||
} catch {
|
||||
Write-LxsWarn "$($svc.Label): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
$disabled = 0
|
||||
foreach ($taskPath in $LxsTrackingTasks) {
|
||||
$leaf = Split-Path $taskPath -Leaf
|
||||
$parent = (Split-Path $taskPath -Parent) + '\'
|
||||
try {
|
||||
Disable-ScheduledTask -TaskName $leaf -TaskPath $parent -ErrorAction Stop | Out-Null
|
||||
$disabled++
|
||||
} catch {
|
||||
# Absent on this edition/build — nothing to disable.
|
||||
}
|
||||
}
|
||||
Write-LxsOk "$disabled tracking scheduled task(s) disabled"
|
||||
}
|
||||
|
||||
function Restore-LxsTweaks {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'REVERT PRIVACY TWEAKS'
|
||||
Write-Host ''
|
||||
Write-Host 'This restores every registry value, service and scheduled task that'
|
||||
Write-Host 'this script changes, back to the Windows default.'
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Apps you removed are NOT restored — reinstall them from the Store.$($script:NC)"
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Revert the privacy tweaks?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
foreach ($e in $LxsTweaks) {
|
||||
if ($null -eq $e.Default) {
|
||||
try {
|
||||
if (Test-Path $e.Path) {
|
||||
Remove-ItemProperty -Path $e.Path -Name $e.Name -ErrorAction Stop
|
||||
}
|
||||
Write-LxsOk "$($e.Label) — policy removed"
|
||||
} catch {
|
||||
# Already absent: that is the default state we wanted.
|
||||
}
|
||||
} else {
|
||||
if (Set-LxsRegistryValue -Path $e.Path -Name $e.Name -Value $e.Default) {
|
||||
Write-LxsOk "$($e.Label) — restored to default"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
foreach ($svc in $LxsTrackingServices) {
|
||||
try {
|
||||
Set-Service -Name $svc.Name -StartupType $svc.Default -ErrorAction Stop
|
||||
Write-LxsOk "$($svc.Label) service restored to $($svc.Default)"
|
||||
} catch {
|
||||
Write-LxsWarn "$($svc.Label): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
$enabled = 0
|
||||
foreach ($taskPath in $LxsTrackingTasks) {
|
||||
$leaf = Split-Path $taskPath -Leaf
|
||||
$parent = (Split-Path $taskPath -Parent) + '\'
|
||||
try {
|
||||
Enable-ScheduledTask -TaskName $leaf -TaskPath $parent -ErrorAction Stop | Out-Null
|
||||
$enabled++
|
||||
} catch {
|
||||
# Not present on this build.
|
||||
}
|
||||
}
|
||||
Write-LxsOk "$enabled scheduled task(s) re-enabled"
|
||||
Write-Host ''
|
||||
Write-LxsWarn 'Sign out and back in for the per-user settings to take effect.'
|
||||
}
|
||||
|
||||
function Show-LxsDebloatStatus {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'CURRENT STATE'
|
||||
Write-Host ''
|
||||
$installed = @(Get-LxsInstalledBloat)
|
||||
Write-Host " Known preinstalled apps still present : $($installed.Count) / $($LxsRemovableApps.Count)"
|
||||
|
||||
$applied = 0
|
||||
foreach ($e in $LxsTweaks) {
|
||||
try {
|
||||
$current = (Get-ItemProperty -Path $e.Path -Name $e.Name -ErrorAction Stop).($e.Name)
|
||||
if ($current -eq $e.Value) { $applied++ }
|
||||
} catch {
|
||||
# Value absent -> tweak not applied.
|
||||
}
|
||||
}
|
||||
Write-Host " Privacy tweaks applied : $applied / $($LxsTweaks.Count)"
|
||||
|
||||
foreach ($svc in $LxsTrackingServices) {
|
||||
try {
|
||||
$s = Get-Service -Name $svc.Name -ErrorAction Stop
|
||||
Write-Host " $($svc.Name.PadRight(38)): $($s.Status) / $($s.StartType)"
|
||||
} catch {
|
||||
Write-Host " $($svc.Name.PadRight(38)): not present"
|
||||
}
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Show-LxsDebloatMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DEBLOAT & PRIVACY' -Right 'ADMIN'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'Show current state'
|
||||
Show-LxsMenuItem '2' 'Remove preinstalled apps' 'you pick each one'
|
||||
Show-LxsMenuItem '3' 'Disable telemetry' 'policy, services, tasks'
|
||||
Show-LxsMenuItem '4' 'Disable Cortana & web search'
|
||||
Show-LxsMenuItem '5' 'Disable ads & suggestions'
|
||||
Show-LxsMenuItem '6' 'Disable Copilot & Recall'
|
||||
Show-LxsMenuItem '7' 'Create a restore point' 'before doing anything'
|
||||
Show-LxsMenuItem '8' 'Revert privacy tweaks' 'undo 3-6'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Show-LxsDebloatStatus; Read-LxsEnter }
|
||||
'2' { Remove-LxsSelectedApps; Read-LxsEnter }
|
||||
'3' { Invoke-LxsTweakGroup -Group 'telemetry' -Title 'DISABLE TELEMETRY'; Read-LxsEnter }
|
||||
'4' { Invoke-LxsTweakGroup -Group 'cortana' -Title 'DISABLE CORTANA & WEB SEARCH'; Read-LxsEnter }
|
||||
'5' { Invoke-LxsTweakGroup -Group 'ads' -Title 'DISABLE ADS & SUGGESTIONS'; Read-LxsEnter }
|
||||
'6' { Invoke-LxsTweakGroup -Group 'ai' -Title 'DISABLE COPILOT & RECALL'; Read-LxsEnter }
|
||||
'7' {
|
||||
if (New-LxsRestorePoint -Description 'LXS manual checkpoint') { $script:LxsCheckpointDone = $true }
|
||||
Read-LxsEnter
|
||||
}
|
||||
'8' { Restore-LxsTweaks; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Show-LxsDebloatMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,327 @@
|
||||
<#
|
||||
LXS - Harden Windows
|
||||
Description: Apply a baseline security posture (firewall, SMBv1, UAC,
|
||||
Defender, LLMNR/NetBIOS) and audit local accounts.
|
||||
Mirror of linux/tools/harden.sh.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
|
||||
Usage: harden.ps1 [-Yes] [-NoFirewall] [-NoSmb] [-NoUac] [-NoDefender]
|
||||
[-NoNameResolution] [-NoRestorePoint] [-Help]
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_harden.log'
|
||||
|
||||
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
|
||||
'-Yes', '-y', '-NoFirewall', '-NoSmb', '-NoUac', '-NoDefender',
|
||||
'-NoNameResolution', '-NoRestorePoint', '-Help', '-h'
|
||||
)
|
||||
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
|
||||
$AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
|
||||
$DoFirewall = -not (Test-LxsFlag $LxsOpts @('-NoFirewall'))
|
||||
$DoSmb = -not (Test-LxsFlag $LxsOpts @('-NoSmb'))
|
||||
$DoUac = -not (Test-LxsFlag $LxsOpts @('-NoUac'))
|
||||
$DoDefender = -not (Test-LxsFlag $LxsOpts @('-NoDefender'))
|
||||
$DoNameResolution = -not (Test-LxsFlag $LxsOpts @('-NoNameResolution'))
|
||||
$DoRestorePoint = -not (Test-LxsFlag $LxsOpts @('-NoRestorePoint'))
|
||||
|
||||
if (Test-LxsFlag $LxsOpts @('-Help', '-h')) {
|
||||
Write-Host @'
|
||||
Usage: harden.ps1 [options]
|
||||
|
||||
Options:
|
||||
-Yes, -y Skip the confirmation prompt
|
||||
-NoFirewall Skip the Windows Firewall configuration
|
||||
-NoSmb Skip disabling SMBv1
|
||||
-NoUac Skip raising the UAC prompt level
|
||||
-NoDefender Skip the Microsoft Defender settings
|
||||
-NoNameResolution Skip disabling LLMNR and NetBIOS-over-TCP/IP
|
||||
-NoRestorePoint Do not create a restore point first
|
||||
-Help, -h Show this help
|
||||
'@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Plan
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'HARDEN WINDOWS' -Right 'ADMIN'
|
||||
Write-Host ''
|
||||
Write-Host 'The following changes will be applied to this machine:'
|
||||
if ($DoFirewall) {
|
||||
Write-Host ' - Firewall: enabled on Domain/Private/Public, inbound blocked by default'
|
||||
}
|
||||
if ($DoSmb) {
|
||||
Write-Host ' - SMBv1: client and server disabled (legacy, exploited by WannaCry/EternalBlue)'
|
||||
}
|
||||
if ($DoUac) {
|
||||
Write-Host ' - UAC: prompt for consent on the secure desktop, never silently elevate'
|
||||
}
|
||||
if ($DoDefender) {
|
||||
Write-Host ' - Defender: real-time protection, PUA blocking, cloud protection, network protection'
|
||||
}
|
||||
if ($DoNameResolution) {
|
||||
Write-Host ' - LLMNR and NetBIOS-over-TCP/IP disabled (blocks classic LAN credential relay)'
|
||||
}
|
||||
Write-Host ' - Audit: local administrators, blank passwords, Guest account (report only)'
|
||||
Write-Host ''
|
||||
if ($DoNameResolution) {
|
||||
Write-Host "$($script:Gray)Note: disabling LLMNR/NetBIOS can break flat-name resolution on small$($script:NC)"
|
||||
Write-Host "$($script:Gray)LANs without a DNS server. Pass -NoNameResolution to keep them.$($script:NC)"
|
||||
Write-Host ''
|
||||
}
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
|
||||
if (-not (Confirm-LxsAction -Question 'Apply this hardening baseline?' -AssumeYes:$AssumeYes)) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($DoRestorePoint) {
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Creating a restore point first...'
|
||||
New-LxsRestorePoint -Description 'LXS before harden' | Out-Null
|
||||
}
|
||||
|
||||
$changes = 0
|
||||
$problems = 0
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Firewall
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if ($DoFirewall) {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'FIREWALL'
|
||||
Write-Host ''
|
||||
try {
|
||||
Set-NetFirewallProfile -Profile Domain, Private, Public -Enabled True `
|
||||
-DefaultInboundAction Block -DefaultOutboundAction Allow -ErrorAction Stop
|
||||
Write-LxsOk 'Firewall enabled on all profiles, inbound blocked by default'
|
||||
$changes++
|
||||
} catch {
|
||||
Write-LxsErr "Firewall configuration failed: $($_.Exception.Message)"
|
||||
$problems++
|
||||
}
|
||||
|
||||
try {
|
||||
Get-NetFirewallProfile -ErrorAction Stop | ForEach-Object {
|
||||
Write-Host " $($_.Name.PadRight(8)) enabled=$($_.Enabled) inbound=$($_.DefaultInboundAction) outbound=$($_.DefaultOutboundAction)"
|
||||
}
|
||||
} catch {
|
||||
# Reporting only.
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# SMBv1
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if ($DoSmb) {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'SMBv1'
|
||||
Write-Host ''
|
||||
try {
|
||||
Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force -ErrorAction Stop
|
||||
Write-LxsOk 'SMBv1 server protocol disabled'
|
||||
$changes++
|
||||
} catch {
|
||||
Write-LxsWarn "Could not disable the SMBv1 server: $($_.Exception.Message)"
|
||||
$problems++
|
||||
}
|
||||
|
||||
try {
|
||||
$feature = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction Stop
|
||||
if ($feature.State -eq 'Enabled') {
|
||||
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction Stop | Out-Null
|
||||
Write-LxsOk 'SMBv1 client feature disabled (reboot to complete)'
|
||||
$changes++
|
||||
} else {
|
||||
Write-LxsOk 'SMBv1 client feature was already disabled'
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "Could not query or disable the SMB1Protocol feature: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# UAC
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if ($DoUac) {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'UAC'
|
||||
Write-Host ''
|
||||
$uacPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
|
||||
$uacOk = $true
|
||||
# EnableLUA=1 keeps UAC on, ConsentPromptBehaviorAdmin=2 always prompts on
|
||||
# the secure desktop, PromptOnSecureDesktop=1 makes that desktop mandatory.
|
||||
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'EnableLUA' -Value 1) -and $uacOk
|
||||
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'ConsentPromptBehaviorAdmin' -Value 2) -and $uacOk
|
||||
$uacOk = (Set-LxsRegistryValue -Path $uacPath -Name 'PromptOnSecureDesktop' -Value 1) -and $uacOk
|
||||
if ($uacOk) {
|
||||
Write-LxsOk 'UAC set to prompt for consent on the secure desktop'
|
||||
$changes++
|
||||
} else {
|
||||
$problems++
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Defender
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if ($DoDefender) {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'DEFENDER'
|
||||
Write-Host ''
|
||||
if (-not (Get-Command Set-MpPreference -ErrorAction SilentlyContinue)) {
|
||||
Write-LxsWarn 'Microsoft Defender cmdlets are not available (third-party AV installed?). Skipped.'
|
||||
} else {
|
||||
$settings = @(
|
||||
@{ Name = 'Real-time protection'; Args = @{ DisableRealtimeMonitoring = $false } },
|
||||
@{ Name = 'PUA blocking'; Args = @{ PUAProtection = 1 } },
|
||||
@{ Name = 'Cloud protection'; Args = @{ MAPSReporting = 2 } },
|
||||
@{ Name = 'Sample submission'; Args = @{ SubmitSamplesConsent = 1 } },
|
||||
@{ Name = 'Network protection'; Args = @{ EnableNetworkProtection = 1 } },
|
||||
@{ Name = 'Script scanning'; Args = @{ DisableScriptScanning = $false } },
|
||||
@{ Name = 'Archive scanning'; Args = @{ DisableArchiveScanning = $false } }
|
||||
)
|
||||
foreach ($s in $settings) {
|
||||
try {
|
||||
$mpArgs = $s.Args
|
||||
Set-MpPreference @mpArgs -ErrorAction Stop
|
||||
Write-LxsOk "$($s.Name) enabled"
|
||||
$changes++
|
||||
} catch {
|
||||
# Tamper Protection blocks these writes by design; that is a
|
||||
# stronger guarantee than what we were trying to set.
|
||||
Write-LxsWarn "$($s.Name): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
try {
|
||||
$status = Get-MpComputerStatus -ErrorAction Stop
|
||||
Write-Host ''
|
||||
Write-Host " Antivirus enabled : $($status.AntivirusEnabled)"
|
||||
Write-Host " Real-time protection : $($status.RealTimeProtectionEnabled)"
|
||||
Write-Host " Tamper protection : $($status.IsTamperProtected)"
|
||||
Write-Host " Signature age (days) : $($status.AntivirusSignatureAge)"
|
||||
} catch {
|
||||
# Reporting only.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# LLMNR + NetBIOS
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if ($DoNameResolution) {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'NAME RESOLUTION'
|
||||
Write-Host ''
|
||||
if (Set-LxsRegistryValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient' `
|
||||
-Name 'EnableMulticast' -Value 0) {
|
||||
Write-LxsOk 'LLMNR disabled'
|
||||
$changes++
|
||||
} else {
|
||||
$problems++
|
||||
}
|
||||
|
||||
# NetbiosOptions: 0 = DHCP default, 1 = enabled, 2 = disabled.
|
||||
$nbPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces'
|
||||
try {
|
||||
$interfaces = @(Get-ChildItem $nbPath -ErrorAction Stop)
|
||||
foreach ($iface in $interfaces) {
|
||||
Set-ItemProperty -Path $iface.PSPath -Name 'NetbiosOptions' -Value 2 -ErrorAction SilentlyContinue
|
||||
}
|
||||
Write-LxsOk "NetBIOS-over-TCP/IP disabled on $($interfaces.Count) interface(s)"
|
||||
$changes++
|
||||
} catch {
|
||||
Write-LxsWarn "Could not disable NetBIOS-over-TCP/IP: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Account audit — reports only, changes nothing.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'ACCOUNT AUDIT'
|
||||
Write-Host ''
|
||||
|
||||
try {
|
||||
$admins = @(Get-LocalGroupMember -Group 'Administrators' -ErrorAction Stop)
|
||||
Write-Host "$($script:Cyan)Local administrators:$($script:NC)"
|
||||
foreach ($a in $admins) { Write-Host " - $($a.Name) $($script:Gray)($($a.ObjectClass), $($a.PrincipalSource))$($script:NC)" }
|
||||
if ($admins.Count -gt 2) {
|
||||
Write-Host ''
|
||||
Write-LxsWarn "$($admins.Count) accounts have administrator rights — review whether they all need it."
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "Could not enumerate the Administrators group: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
try {
|
||||
$locals = @(Get-LocalUser -ErrorAction Stop)
|
||||
$enabledNoExpiry = @($locals | Where-Object { $_.Enabled -and $_.PasswordNeverExpires })
|
||||
$noPassword = @($locals | Where-Object { $_.Enabled -and -not $_.PasswordLastSet -and $_.Name -ne 'DefaultAccount' })
|
||||
$guest = $locals | Where-Object { $_.Name -eq 'Guest' }
|
||||
|
||||
if ($noPassword.Count -gt 0) {
|
||||
Write-LxsWarn "Enabled accounts that have never set a password: $($noPassword.Name -join ', ')"
|
||||
} else {
|
||||
Write-LxsOk 'No enabled account is missing a password'
|
||||
}
|
||||
if ($enabledNoExpiry.Count -gt 0) {
|
||||
Write-Host "$($script:Gray) Password never expires: $($enabledNoExpiry.Name -join ', ')$($script:NC)"
|
||||
}
|
||||
if ($guest -and $guest.Enabled) {
|
||||
Write-LxsWarn 'The Guest account is ENABLED — disable it unless you rely on it.'
|
||||
} else {
|
||||
Write-LxsOk 'Guest account is disabled'
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "Could not enumerate local users: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
Write-LxsOk "$changes hardening change(s) applied"
|
||||
if ($problems -gt 0) {
|
||||
Write-LxsWarn "$problems change(s) failed — see the messages above."
|
||||
}
|
||||
if ($DoSmb) {
|
||||
Write-LxsWarn 'Reboot to finish removing the SMBv1 client feature.'
|
||||
}
|
||||
Write-Host ''
|
||||
exit 0
|
||||
@@ -0,0 +1,75 @@
|
||||
<#
|
||||
LXS - Tools index (Windows)
|
||||
Description: Interactive menu listing the scripts in windows\tools
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
function Show-LxsToolsMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'TOOLS' -Right 'WIN_TOOLBOX'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '01' 'System Infos' 'hardware, os, processes'
|
||||
Show-LxsMenuItem '02' 'Network Diag' 'ip, dns, ports, repair'
|
||||
Show-LxsMenuItem '03' 'Disk Cleanup' 'temp, update cache, winsxs'
|
||||
Show-LxsMenuItem '04' 'System Repair' 'sfc, dism, chkdsk'
|
||||
Show-LxsMenuItem '05' 'Restore Point' 'create, list, roll back'
|
||||
Show-LxsMenuItem '06' 'Update Windows' 'windows update + winget'
|
||||
Show-LxsMenuItem '07' 'Harden Windows' 'firewall, smb, defender'
|
||||
Show-LxsMenuItem '08' 'Remote Access' 'rdp + openssh server'
|
||||
Show-LxsMenuItem '09' 'Debloat' 'remove apps, telemetry off'
|
||||
Show-LxsMenuItem '00' 'BACK' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
|
||||
$script = switch -Regex ($choice) {
|
||||
'^0?1$' { 'tools\system-info.ps1' }
|
||||
'^0?2$' { 'tools\net-diag.ps1' }
|
||||
'^0?3$' { 'tools\cleanup.ps1' }
|
||||
'^0?4$' { 'tools\repair.ps1' }
|
||||
'^0?5$' { 'tools\restore-point.ps1' }
|
||||
'^0?6$' { 'tools\update-windows.ps1' }
|
||||
'^0?7$' { 'tools\harden.ps1' }
|
||||
'^0?8$' { 'tools\remote-access.ps1' }
|
||||
'^0?9$' { 'tools\debloat.ps1' }
|
||||
'^0?0$' { 'BACK' }
|
||||
default { $null }
|
||||
}
|
||||
|
||||
if ($script -eq 'BACK') { return }
|
||||
if (-not $script) {
|
||||
Write-LxsErr 'Invalid protocol. Select 0-9.'
|
||||
Start-Sleep -Seconds 1
|
||||
continue
|
||||
}
|
||||
|
||||
$code = Invoke-LxsSibling -RelativePath $script -SelfDirectory $PSScriptRoot
|
||||
|
||||
# Exit code 75 from a child means it already paused on its own
|
||||
# (its own "Back" or end-of-run prompt) — skip the redundant prompt.
|
||||
if ($code -ne 75) { Read-LxsEnter }
|
||||
}
|
||||
}
|
||||
|
||||
Show-LxsToolsMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,197 @@
|
||||
<#
|
||||
LXS - Network Diagnostics (Windows)
|
||||
Description: Inspect and repair the network stack.
|
||||
Everything is read-only except option 8 (stack reset).
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_net_diag.log'
|
||||
|
||||
function Show-LxsIpConfiguration {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'IP CONFIGURATION'
|
||||
Write-Host ''
|
||||
try {
|
||||
Get-NetIPConfiguration -Detailed -ErrorAction Stop | ForEach-Object {
|
||||
Write-Host "$($script:Cyan)$($script:Bold)$($_.InterfaceAlias)$($script:NC) $($script:Gray)($($_.InterfaceDescription))$($script:NC)"
|
||||
Write-Host " Status : $($_.NetAdapter.Status) | MAC: $($_.NetAdapter.MacAddress) | Speed: $($_.NetAdapter.LinkSpeed)"
|
||||
Write-Host " IPv4 : $($_.IPv4Address.IPAddress -join ', ')"
|
||||
Write-Host " IPv6 : $($_.IPv6Address.IPAddress -join ', ')"
|
||||
Write-Host " Gateway : $($_.IPv4DefaultGateway.NextHop -join ', ')"
|
||||
Write-Host " DNS : $($_.DNSServer.ServerAddresses -join ', ')"
|
||||
Write-Host ''
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn 'Get-NetIPConfiguration unavailable; falling back to ipconfig /all.'
|
||||
ipconfig /all | Out-String | Write-Host
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsListeningPorts {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'LISTENING PORTS'
|
||||
Write-Host ''
|
||||
try {
|
||||
$rows = Get-NetTCPConnection -State Listen -ErrorAction Stop | ForEach-Object {
|
||||
$procName = try { (Get-Process -Id $_.OwningProcess -ErrorAction Stop).ProcessName } catch { 'unknown' }
|
||||
[pscustomobject]@{
|
||||
Local = "$($_.LocalAddress):$($_.LocalPort)"
|
||||
Port = $_.LocalPort
|
||||
PID = $_.OwningProcess
|
||||
Process = $procName
|
||||
}
|
||||
}
|
||||
$rows | Sort-Object Port | Format-Table -AutoSize Local, PID, Process | Out-String | Write-Host
|
||||
} catch {
|
||||
Write-LxsWarn 'Get-NetTCPConnection unavailable; falling back to netstat.'
|
||||
netstat -ano | Select-String 'LISTENING' | Out-String | Write-Host
|
||||
}
|
||||
}
|
||||
|
||||
function Clear-LxsDnsCache {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'FLUSH DNS CACHE'
|
||||
Write-Host ''
|
||||
try {
|
||||
Clear-DnsClientCache -ErrorAction Stop
|
||||
Write-LxsOk 'DNS resolver cache flushed'
|
||||
} catch {
|
||||
Write-LxsWarn 'Clear-DnsClientCache failed; falling back to ipconfig /flushdns.'
|
||||
ipconfig /flushdns | Out-String | Write-Host
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-LxsPing {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'PING'
|
||||
Write-Host ''
|
||||
$target = Read-Host -Prompt 'Host to ping (default: 1.1.1.1)'
|
||||
if ([string]::IsNullOrWhiteSpace($target)) { $target = '1.1.1.1' }
|
||||
Write-Host ''
|
||||
ping.exe -n 4 $target | Out-String | Write-Host
|
||||
}
|
||||
|
||||
function Invoke-LxsTraceroute {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'TRACEROUTE'
|
||||
Write-Host ''
|
||||
$target = Read-Host -Prompt 'Host to trace (default: 1.1.1.1)'
|
||||
if ([string]::IsNullOrWhiteSpace($target)) { $target = '1.1.1.1' }
|
||||
Write-Host ''
|
||||
Write-LxsInfo "Tracing route to $target (this can take a minute)..."
|
||||
tracert.exe -d -h 20 $target | Out-String | Write-Host
|
||||
}
|
||||
|
||||
function Test-LxsRemotePort {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'TEST TCP PORT'
|
||||
Write-Host ''
|
||||
$target = Read-Host -Prompt 'Host'
|
||||
if ([string]::IsNullOrWhiteSpace($target)) { Write-LxsErr 'No host given.'; return }
|
||||
$port = Read-Host -Prompt 'Port'
|
||||
if ($port -notmatch '^\d+$') { Write-LxsErr 'Port must be a number.'; return }
|
||||
Write-Host ''
|
||||
try {
|
||||
$result = Test-NetConnection -ComputerName $target -Port ([int]$port) -WarningAction SilentlyContinue -ErrorAction Stop
|
||||
if ($result.TcpTestSucceeded) {
|
||||
Write-LxsOk "${target}:${port} is reachable ($($result.RemoteAddress))"
|
||||
} else {
|
||||
Write-LxsErr "${target}:${port} is NOT reachable"
|
||||
}
|
||||
} catch {
|
||||
Write-LxsErr "Test failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsPublicIp {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'PUBLIC IP'
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Querying...'
|
||||
Write-Host ''
|
||||
Write-Host " $($script:White)$($script:Bold)$(Get-LxsPublicIP)$($script:NC)"
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Reset-LxsNetworkStack {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'RESET NETWORK STACK' -Right 'DESTRUCTIVE'
|
||||
Write-Host ''
|
||||
Write-Host 'This resets the Winsock catalog and the TCP/IP stack. It clears'
|
||||
Write-Host 'third-party LSP entries, static IP settings and proxy configuration,'
|
||||
Write-Host "and $($script:Bold)requires a reboot$($script:NC) to take effect."
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Reset the network stack now?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
if (-not (Test-LxsAdmin)) {
|
||||
Write-LxsErr 'This action requires administrator rights. Re-run lxs from an elevated terminal.'
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
netsh winsock reset | Out-String | Write-Host
|
||||
netsh int ip reset | Out-String | Write-Host
|
||||
Clear-DnsClientCache -ErrorAction SilentlyContinue
|
||||
Write-Host ''
|
||||
Write-LxsOk 'Network stack reset'
|
||||
Write-LxsWarn 'Reboot required for the change to take effect.'
|
||||
}
|
||||
|
||||
function Show-LxsNetDiagMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'NETWORK DIAG'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'Show IP configuration'
|
||||
Show-LxsMenuItem '2' 'Show listening ports'
|
||||
Show-LxsMenuItem '3' 'Flush DNS cache'
|
||||
Show-LxsMenuItem '4' 'Ping a host'
|
||||
Show-LxsMenuItem '5' 'Traceroute a host'
|
||||
Show-LxsMenuItem '6' 'Test a TCP port'
|
||||
Show-LxsMenuItem '7' 'Show public IP'
|
||||
Show-LxsMenuItem '8' 'Reset network stack' 'needs admin + reboot'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Show-LxsIpConfiguration; Read-LxsEnter }
|
||||
'2' { Show-LxsListeningPorts; Read-LxsEnter }
|
||||
'3' { Clear-LxsDnsCache; Read-LxsEnter }
|
||||
'4' { Invoke-LxsPing; Read-LxsEnter }
|
||||
'5' { Invoke-LxsTraceroute; Read-LxsEnter }
|
||||
'6' { Test-LxsRemotePort; Read-LxsEnter }
|
||||
'7' { Show-LxsPublicIp; Read-LxsEnter }
|
||||
'8' { Reset-LxsNetworkStack; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Show-LxsNetDiagMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,300 @@
|
||||
<#
|
||||
LXS - Remote Access (Windows)
|
||||
Description: Remote Desktop and the OpenSSH server — status, enable,
|
||||
disable. The Windows counterpart of root-ssh-login.sh.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_remote_access.log'
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
|
||||
|
||||
$LxsRdpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
|
||||
$LxsRdpTcpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
|
||||
|
||||
function Get-LxsRdpState {
|
||||
try {
|
||||
# fDenyTSConnections 0 = RDP allowed, 1 = denied.
|
||||
$deny = (Get-ItemProperty -Path $LxsRdpKey -Name 'fDenyTSConnections' -ErrorAction Stop).fDenyTSConnections
|
||||
return ($deny -eq 0)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Get-LxsNlaState {
|
||||
try {
|
||||
return ((Get-ItemProperty -Path $LxsRdpTcpKey -Name 'UserAuthentication' -ErrorAction Stop).UserAuthentication -eq 1)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsRemoteStatus {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'REMOTE ACCESS STATUS'
|
||||
Write-Host ''
|
||||
|
||||
Write-Host "$($script:Cyan)$($script:Bold)Remote Desktop$($script:NC)"
|
||||
if (Get-LxsRdpState) {
|
||||
Write-LxsOk 'RDP is ENABLED'
|
||||
if (Get-LxsNlaState) {
|
||||
Write-LxsOk 'Network Level Authentication is on'
|
||||
} else {
|
||||
Write-LxsWarn 'Network Level Authentication is OFF — turn it on (option 2).'
|
||||
}
|
||||
try {
|
||||
$port = (Get-ItemProperty -Path $LxsRdpTcpKey -Name 'PortNumber' -ErrorAction Stop).PortNumber
|
||||
Write-Host " Listening port: $port"
|
||||
} catch {
|
||||
Write-Host ' Listening port: 3389 (default)'
|
||||
}
|
||||
} else {
|
||||
Write-Host " $($script:Gray)RDP is disabled$($script:NC)"
|
||||
}
|
||||
|
||||
try {
|
||||
$rules = @(Get-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop | Where-Object { $_.Enabled -eq 'True' })
|
||||
Write-Host " Firewall rules enabled: $($rules.Count)"
|
||||
} catch {
|
||||
# Group may not exist on Server Core images.
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Cyan)$($script:Bold)OpenSSH Server$($script:NC)"
|
||||
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
|
||||
if (-not $svc) {
|
||||
Write-Host " $($script:Gray)Not installed$($script:NC)"
|
||||
} else {
|
||||
if ($svc.Status -eq 'Running') { Write-LxsOk "sshd is running (startup: $($svc.StartType))" }
|
||||
else { Write-LxsWarn "sshd is installed but $($svc.Status) (startup: $($svc.StartType))" }
|
||||
try {
|
||||
$shell = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -ErrorAction Stop).DefaultShell
|
||||
Write-Host " Default shell : $shell"
|
||||
} catch {
|
||||
Write-Host " Default shell : cmd.exe (Windows default)"
|
||||
}
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Enable-LxsRdp {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'ENABLE REMOTE DESKTOP'
|
||||
Write-Host ''
|
||||
Write-Host 'This allows inbound RDP connections and opens the matching'
|
||||
Write-Host 'firewall rules. Network Level Authentication will be required,'
|
||||
Write-Host 'so clients must authenticate before a session is created.'
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Enable Remote Desktop?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
Write-Host ''
|
||||
if (Set-LxsRegistryValue -Path $LxsRdpKey -Name 'fDenyTSConnections' -Value 0) {
|
||||
Write-LxsOk 'RDP connections allowed'
|
||||
}
|
||||
if (Set-LxsRegistryValue -Path $LxsRdpTcpKey -Name 'UserAuthentication' -Value 1) {
|
||||
Write-LxsOk 'Network Level Authentication required'
|
||||
}
|
||||
try {
|
||||
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop
|
||||
Write-LxsOk 'Firewall rules for Remote Desktop enabled'
|
||||
} catch {
|
||||
Write-LxsWarn "Could not enable the firewall rules: $($_.Exception.Message)"
|
||||
}
|
||||
Write-Host ''
|
||||
Write-LxsWarn 'Only expose RDP to the internet behind a VPN — it is a constant brute-force target.'
|
||||
}
|
||||
|
||||
function Disable-LxsRdp {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISABLE REMOTE DESKTOP'
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Disable Remote Desktop and its firewall rules?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
Write-Host ''
|
||||
if (Set-LxsRegistryValue -Path $LxsRdpKey -Name 'fDenyTSConnections' -Value 1) {
|
||||
Write-LxsOk 'RDP connections denied'
|
||||
}
|
||||
try {
|
||||
Disable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction Stop
|
||||
Write-LxsOk 'Firewall rules for Remote Desktop disabled'
|
||||
} catch {
|
||||
Write-LxsWarn "Could not disable the firewall rules: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Install-LxsSshServer {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'OPENSSH SERVER'
|
||||
Write-Host ''
|
||||
Write-Host 'Installs the OpenSSH Server capability, starts sshd, sets it to'
|
||||
Write-Host 'start automatically and opens TCP/22 in the firewall.'
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Install and enable the OpenSSH server?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
Write-Host ''
|
||||
|
||||
try {
|
||||
$cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' -ErrorAction Stop |
|
||||
Select-Object -First 1
|
||||
} catch {
|
||||
Write-LxsErr "Could not query Windows capabilities: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $cap) {
|
||||
Write-LxsErr 'The OpenSSH Server capability is not available on this edition.'
|
||||
return
|
||||
}
|
||||
|
||||
if ($cap.State -ne 'Installed') {
|
||||
Write-LxsInfo "Installing $($cap.Name) (this can take a few minutes)..."
|
||||
try {
|
||||
Add-WindowsCapability -Online -Name $cap.Name -ErrorAction Stop | Out-Null
|
||||
Write-LxsOk 'OpenSSH Server installed'
|
||||
} catch {
|
||||
Write-LxsErr "Installation failed: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Write-LxsOk 'OpenSSH Server was already installed'
|
||||
}
|
||||
|
||||
try {
|
||||
Set-Service -Name sshd -StartupType Automatic -ErrorAction Stop
|
||||
Start-Service -Name sshd -ErrorAction Stop
|
||||
Write-LxsOk 'sshd is running and set to start automatically'
|
||||
} catch {
|
||||
Write-LxsErr "Could not start sshd: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
|
||||
# The capability normally creates this rule; create it when it is missing.
|
||||
try {
|
||||
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
|
||||
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
|
||||
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -ErrorAction Stop | Out-Null
|
||||
Write-LxsOk 'Firewall rule created for TCP/22'
|
||||
} else {
|
||||
Write-LxsOk 'Firewall rule for TCP/22 already present'
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn "Could not create the firewall rule: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Connect with: ssh $env:USERNAME@$(Get-LxsPublicIP)$($script:NC)"
|
||||
Write-Host "$($script:Gray)Key-based auth: put your public key in %ProgramData%\ssh\administrators_authorized_keys$($script:NC)"
|
||||
Write-Host "$($script:Gray)for admin accounts, or in %USERPROFILE%\.ssh\authorized_keys otherwise.$($script:NC)"
|
||||
}
|
||||
|
||||
function Disable-LxsSshServer {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISABLE OPENSSH SERVER'
|
||||
Write-Host ''
|
||||
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
|
||||
if (-not $svc) {
|
||||
Write-LxsWarn 'The OpenSSH server is not installed.'
|
||||
return
|
||||
}
|
||||
if (-not (Confirm-LxsAction -Question 'Stop sshd and set it to Disabled?')) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
Stop-Service -Name sshd -Force -ErrorAction Stop
|
||||
Set-Service -Name sshd -StartupType Disabled -ErrorAction Stop
|
||||
Write-LxsOk 'sshd stopped and disabled'
|
||||
} catch {
|
||||
Write-LxsErr "Failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Set-LxsSshDefaultShell {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SSH DEFAULT SHELL'
|
||||
Write-Host ''
|
||||
Write-Host 'Which shell should SSH sessions land in?'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'PowerShell' 'powershell.exe'
|
||||
Show-LxsMenuItem '2' 'PowerShell 7' 'pwsh.exe (must be installed)'
|
||||
Show-LxsMenuItem '3' 'Command Prompt' 'cmd.exe (Windows default)'
|
||||
Show-LxsMenuItem '0' 'Cancel' '' -Exit
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
|
||||
$shell = switch ($choice) {
|
||||
'1' { "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" }
|
||||
'2' { (Get-Command pwsh -ErrorAction SilentlyContinue).Source }
|
||||
'3' { "$env:SystemRoot\System32\cmd.exe" }
|
||||
default { $null }
|
||||
}
|
||||
if (-not $shell) {
|
||||
if ($choice -eq '2') { Write-LxsErr 'pwsh.exe was not found — install PowerShell 7 first.' }
|
||||
else { Write-LxsInfo 'Cancelled.' }
|
||||
return
|
||||
}
|
||||
Write-Host ''
|
||||
if (Set-LxsRegistryValue -Path 'HKLM:\SOFTWARE\OpenSSH' -Name 'DefaultShell' -Value $shell -Type String) {
|
||||
Write-LxsOk "SSH sessions will start: $shell"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsRemoteMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'REMOTE ACCESS' -Right 'ADMIN'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'Show status'
|
||||
Show-LxsMenuItem '2' 'Enable Remote Desktop' 'with NLA + firewall'
|
||||
Show-LxsMenuItem '3' 'Disable Remote Desktop'
|
||||
Show-LxsMenuItem '4' 'Install OpenSSH Server' 'service + firewall'
|
||||
Show-LxsMenuItem '5' 'Disable OpenSSH Server'
|
||||
Show-LxsMenuItem '6' 'Set SSH default shell'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Show-LxsRemoteStatus; Read-LxsEnter }
|
||||
'2' { Enable-LxsRdp; Read-LxsEnter }
|
||||
'3' { Disable-LxsRdp; Read-LxsEnter }
|
||||
'4' { Install-LxsSshServer; Read-LxsEnter }
|
||||
'5' { Disable-LxsSshServer; Read-LxsEnter }
|
||||
'6' { Set-LxsSshDefaultShell; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-6.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Show-LxsRemoteMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,150 @@
|
||||
<#
|
||||
LXS - System Repair (Windows)
|
||||
Description: Integrity repair for the component store and system files
|
||||
(SFC, DISM, chkdsk). Requires administrator rights.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_repair.log'
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
|
||||
|
||||
# These commands stream long-running progress; running them through the
|
||||
# spinner would hide it, so they write straight to the console.
|
||||
function Invoke-LxsRepairCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Title,
|
||||
[Parameter(Mandatory = $true)][string]$Exe,
|
||||
[Parameter(Mandatory = $true)][string[]]$CmdArgs,
|
||||
[string]$Note = ''
|
||||
)
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title $Title
|
||||
Write-Host ''
|
||||
if ($Note) {
|
||||
Write-Host "$($script:Gray)$Note$($script:NC)"
|
||||
Write-Host ''
|
||||
}
|
||||
Write-LxsInfo "$Exe $($CmdArgs -join ' ')"
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
|
||||
& $Exe @CmdArgs
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
if ($code -eq 0) {
|
||||
Write-LxsOk "$Title completed"
|
||||
} else {
|
||||
Write-LxsWarn "$Title exited with code $code"
|
||||
}
|
||||
return $code
|
||||
}
|
||||
|
||||
function Invoke-LxsSfc {
|
||||
Invoke-LxsRepairCommand -Title 'SFC SCANNOW' -Exe 'sfc.exe' -CmdArgs @('/scannow') `
|
||||
-Note 'Verifies every protected system file and repairs corrupted ones from the component store. Takes 5-15 minutes.' | Out-Null
|
||||
}
|
||||
|
||||
function Invoke-LxsDism {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISM RESTOREHEALTH'
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Repairs the component store itself. Run this before SFC when SFC$($script:NC)"
|
||||
Write-Host "$($script:Gray)reports that it cannot fix files. Needs internet access for the$($script:NC)"
|
||||
Write-Host "$($script:Gray)replacement payloads. Takes 10-30 minutes.$($script:NC)"
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
|
||||
Write-LxsInfo 'Step 1/3 - CheckHealth'
|
||||
& dism.exe /Online /Cleanup-Image /CheckHealth
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Step 2/3 - ScanHealth (slow)'
|
||||
& dism.exe /Online /Cleanup-Image /ScanHealth
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Step 3/3 - RestoreHealth'
|
||||
& dism.exe /Online /Cleanup-Image /RestoreHealth
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
if ($code -eq 0) { Write-LxsOk 'DISM completed' } else { Write-LxsWarn "DISM exited with code $code" }
|
||||
}
|
||||
|
||||
function Invoke-LxsChkdsk {
|
||||
Invoke-LxsRepairCommand -Title 'CHKDSK SCAN' -Exe 'chkdsk.exe' -CmdArgs @($env:SystemDrive, '/scan') `
|
||||
-Note 'Online, read-only scan of the system volume. It reports problems without taking the volume offline; a full /f repair needs a reboot.' | Out-Null
|
||||
}
|
||||
|
||||
function Invoke-LxsFullRepair {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'FULL REPAIR PASS'
|
||||
Write-Host ''
|
||||
Write-Host 'Runs, in the recommended order:'
|
||||
Write-Host ' 1. DISM /RestoreHealth (repair the component store)'
|
||||
Write-Host ' 2. SFC /scannow (repair system files from that store)'
|
||||
Write-Host ' 3. chkdsk /scan (check the file system)'
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Budget 30-60 minutes. Nothing here is destructive.$($script:NC)"
|
||||
Write-Host ''
|
||||
if (-not (Confirm-LxsAction -Question 'Start the full repair pass?' -DefaultYes)) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
Invoke-LxsDism
|
||||
Read-LxsEnter
|
||||
Invoke-LxsSfc
|
||||
Read-LxsEnter
|
||||
Invoke-LxsChkdsk
|
||||
}
|
||||
|
||||
function Show-LxsRepairMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SYSTEM REPAIR' -Right 'ADMIN'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'SFC /scannow' 'repair system files'
|
||||
Show-LxsMenuItem '2' 'DISM /RestoreHealth' 'repair component store'
|
||||
Show-LxsMenuItem '3' 'chkdsk /scan' 'check the file system'
|
||||
Show-LxsMenuItem '4' 'Full repair pass' 'DISM, then SFC, then chkdsk'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Invoke-LxsSfc; Read-LxsEnter }
|
||||
'2' { Invoke-LxsDism; Read-LxsEnter }
|
||||
'3' { Invoke-LxsChkdsk; Read-LxsEnter }
|
||||
'4' { Invoke-LxsFullRepair; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-4.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Show-LxsRepairMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,177 @@
|
||||
<#
|
||||
LXS - System Restore Point (Windows)
|
||||
Description: Enable System Protection, create checkpoints, list them and
|
||||
roll back. The safety net for harden.ps1 and debloat.ps1.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_restore_point.log'
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args
|
||||
|
||||
function Get-LxsRestorePoints {
|
||||
try {
|
||||
return @(Get-ComputerRestorePoint -ErrorAction Stop)
|
||||
} catch {
|
||||
return @()
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsRestorePoints {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'RESTORE POINTS'
|
||||
Write-Host ''
|
||||
$points = Get-LxsRestorePoints
|
||||
if ($points.Count -eq 0) {
|
||||
Write-LxsWarn 'No restore points found (System Protection may be disabled).'
|
||||
return
|
||||
}
|
||||
$points | Sort-Object SequenceNumber -Descending |
|
||||
Format-Table -AutoSize `
|
||||
@{ Name = 'Seq'; Expression = { $_.SequenceNumber } },
|
||||
@{ Name = 'Created'; Expression = { $_.ConvertToDateTime($_.CreationTime).ToString('yyyy-MM-dd HH:mm') } },
|
||||
@{ Name = 'Type'; Expression = { $_.RestorePointType } },
|
||||
@{ Name = 'Description'; Expression = { $_.Description } } |
|
||||
Out-String -Width 160 | Write-Host
|
||||
}
|
||||
|
||||
function Show-LxsProtectionStatus {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SYSTEM PROTECTION'
|
||||
Write-Host ''
|
||||
$drive = "$env:SystemDrive\"
|
||||
try {
|
||||
# Volumes listed here have shadow storage configured, i.e. protection on.
|
||||
$shadow = @(Get-CimInstance Win32_ShadowStorage -ErrorAction Stop)
|
||||
if ($shadow.Count -gt 0) {
|
||||
Write-LxsOk "System Protection appears to be ON for $($shadow.Count) volume(s)"
|
||||
foreach ($s in $shadow) {
|
||||
$maxGB = [math]::Round($s.MaxSpace / 1GB, 1)
|
||||
$usedGB = [math]::Round($s.UsedSpace / 1GB, 2)
|
||||
Write-Host " Shadow storage: $usedGB GB used of $maxGB GB allocated"
|
||||
}
|
||||
} else {
|
||||
Write-LxsWarn "System Protection looks OFF for $drive — no restore points can be created."
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn 'Could not read the shadow storage configuration.'
|
||||
}
|
||||
Write-Host ''
|
||||
Write-Host "$($script:Gray)Restore points found: $((Get-LxsRestorePoints).Count)$($script:NC)"
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Enable-LxsProtection {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'ENABLE SYSTEM PROTECTION'
|
||||
Write-Host ''
|
||||
$drive = "$env:SystemDrive\"
|
||||
try {
|
||||
Enable-ComputerRestore -Drive $drive -ErrorAction Stop
|
||||
Write-LxsOk "System Protection enabled on $drive"
|
||||
} catch {
|
||||
Write-LxsErr "Failed to enable System Protection: $($_.Exception.Message)"
|
||||
return
|
||||
}
|
||||
# Default shadow storage can be as low as 1%; 5% leaves room for a few points.
|
||||
try {
|
||||
& vssadmin.exe Resize ShadowStorage /For=$drive /On=$drive /MaxSize=5% | Out-String | Write-Host
|
||||
} catch {
|
||||
Write-LxsWarn 'Could not resize the shadow storage allocation.'
|
||||
}
|
||||
}
|
||||
|
||||
function New-LxsCheckpoint {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'CREATE RESTORE POINT'
|
||||
Write-Host ''
|
||||
$desc = Read-Host -Prompt 'Description (default: LXS manual checkpoint)'
|
||||
if ([string]::IsNullOrWhiteSpace($desc)) { $desc = 'LXS manual checkpoint' }
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Creating the restore point (this can take a minute)...'
|
||||
New-LxsRestorePoint -Description $desc | Out-Null
|
||||
}
|
||||
|
||||
function Restore-LxsCheckpoint {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'ROLL BACK' -Right 'REBOOTS'
|
||||
Write-Host ''
|
||||
$points = Get-LxsRestorePoints
|
||||
if ($points.Count -eq 0) {
|
||||
Write-LxsWarn 'No restore points to roll back to.'
|
||||
return
|
||||
}
|
||||
Show-LxsRestorePoints
|
||||
Write-Host "$($script:Red)Restoring reboots the machine immediately and reverts system files,$($script:NC)"
|
||||
Write-Host "$($script:Red)drivers, registry and installed programs to that point in time.$($script:NC)"
|
||||
Write-Host "$($script:Gray)Your documents are not touched.$($script:NC)"
|
||||
Write-Host ''
|
||||
$seq = Read-Host -Prompt 'Sequence number to restore (empty to cancel)'
|
||||
if ([string]::IsNullOrWhiteSpace($seq)) { Write-LxsInfo 'Cancelled.'; return }
|
||||
if ($seq -notmatch '^\d+$') { Write-LxsErr 'Sequence number must be numeric.'; return }
|
||||
if (-not ($points | Where-Object { $_.SequenceNumber -eq [int]$seq })) {
|
||||
Write-LxsErr "No restore point with sequence number $seq."
|
||||
return
|
||||
}
|
||||
if (-not (Confirm-LxsAction -Question "Restore to point $seq and reboot NOW?")) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
return
|
||||
}
|
||||
try {
|
||||
Restore-Computer -RestorePoint ([int]$seq) -Confirm:$false -ErrorAction Stop
|
||||
} catch {
|
||||
Write-LxsErr "Restore failed: $($_.Exception.Message)"
|
||||
Write-Host "$($script:Gray) You can also use: rstrui.exe$($script:NC)"
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsRestoreMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'RESTORE POINT' -Right 'ADMIN'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'Show protection status'
|
||||
Show-LxsMenuItem '2' 'Enable System Protection'
|
||||
Show-LxsMenuItem '3' 'Create a restore point'
|
||||
Show-LxsMenuItem '4' 'List restore points'
|
||||
Show-LxsMenuItem '5' 'Roll back to a restore point' 'reboots the machine'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Show-LxsProtectionStatus; Read-LxsEnter }
|
||||
'2' { Enable-LxsProtection; Read-LxsEnter }
|
||||
'3' { New-LxsCheckpoint; Read-LxsEnter }
|
||||
'4' { Show-LxsRestorePoints; Read-LxsEnter }
|
||||
'5' { Restore-LxsCheckpoint; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-5.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Show-LxsRestoreMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,277 @@
|
||||
<#
|
||||
LXS - System Infos (Windows)
|
||||
Description: Essential system monitoring and diagnostic views.
|
||||
Read-only: nothing here changes the machine.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_system_infos.log'
|
||||
|
||||
function Write-LxsField {
|
||||
param([string]$Label, $Value)
|
||||
Write-Host "$($script:Cyan)$($script:Bold)${Label}:$($script:NC)"
|
||||
foreach ($line in @($Value)) { Write-Host " $line" }
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Show-LxsSystemInformation {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SYSTEM INFORMATION'
|
||||
Write-Host ''
|
||||
|
||||
try {
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
$cs = Get-CimInstance Win32_ComputerSystem
|
||||
$cpu = @(Get-CimInstance Win32_Processor)
|
||||
$bios = Get-CimInstance Win32_BIOS
|
||||
|
||||
Write-LxsField 'Operating System' "$($os.Caption) $($os.OSArchitecture)"
|
||||
Write-LxsField 'Version / Build' "$($os.Version) (build $($os.BuildNumber))"
|
||||
Write-LxsField 'Installed On' $os.InstallDate
|
||||
$span = (Get-Date) - $os.LastBootUpTime
|
||||
Write-LxsField 'System Uptime' ("{0} days, {1} hours, {2} minutes" -f $span.Days, $span.Hours, $span.Minutes)
|
||||
Write-LxsField 'Hostname' $env:COMPUTERNAME
|
||||
Write-LxsField 'Manufacturer' "$($cs.Manufacturer) $($cs.Model)"
|
||||
Write-LxsField 'BIOS' "$($bios.Manufacturer) $($bios.SMBIOSBIOSVersion)"
|
||||
Write-LxsField 'CPU' ($cpu | ForEach-Object { "$($_.Name.Trim()) ($($_.NumberOfCores)C/$($_.NumberOfLogicalProcessors)T)" })
|
||||
Write-LxsField 'Memory' ("{0} GB total" -f [math]::Round($cs.TotalPhysicalMemory / 1GB, 1))
|
||||
Write-LxsField 'PowerShell' $PSVersionTable.PSVersion.ToString()
|
||||
} catch {
|
||||
Write-LxsErr "Could not read system information: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
try {
|
||||
$gpu = @(Get-CimInstance Win32_VideoController -ErrorAction Stop |
|
||||
ForEach-Object { "$($_.Name) (driver $($_.DriverVersion))" })
|
||||
if ($gpu.Count -gt 0) { Write-LxsField 'GPU' $gpu }
|
||||
} catch {
|
||||
# Headless or restricted WMI — not worth failing the whole view.
|
||||
}
|
||||
|
||||
# Activation status. LicenseStatus 1 = licensed.
|
||||
try {
|
||||
$lic = @(Get-CimInstance SoftwareLicensingProduct -ErrorAction Stop |
|
||||
Where-Object { $_.PartialProductKey -and $_.Name -like 'Windows*' } | Select-Object -First 1)
|
||||
if ($lic.Count -gt 0) {
|
||||
$state = switch ([int]$lic[0].LicenseStatus) {
|
||||
0 { 'Unlicensed' }
|
||||
1 { 'Licensed' }
|
||||
2 { 'Out-of-box grace period' }
|
||||
3 { 'Out-of-tolerance grace period' }
|
||||
4 { 'Non-genuine grace period' }
|
||||
5 { 'Notification (not activated)' }
|
||||
6 { 'Extended grace' }
|
||||
default { 'Unknown' }
|
||||
}
|
||||
Write-LxsField 'Activation' "$state — $($lic[0].Name)"
|
||||
}
|
||||
} catch {
|
||||
# SoftwareLicensingProduct is unavailable on some SKUs.
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsDiskSpace {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISK SPACE'
|
||||
Write-Host ''
|
||||
Get-CimInstance Win32_LogicalDisk -Filter 'DriveType=3' | ForEach-Object {
|
||||
$totalGB = [math]::Round($_.Size / 1GB, 1)
|
||||
$freeGB = [math]::Round($_.FreeSpace / 1GB, 1)
|
||||
$usedGB = [math]::Round(($_.Size - $_.FreeSpace) / 1GB, 1)
|
||||
$pct = if ($_.Size -gt 0) { [math]::Round(100 * ($_.Size - $_.FreeSpace) / $_.Size) } else { 0 }
|
||||
$bar = ('#' * [math]::Round($pct / 5)).PadRight(20, '.')
|
||||
$color = if ($pct -ge 90) { $script:Red } else { $script:Cyan }
|
||||
Write-Host (" $($script:White)$($script:Bold){0,-4}$($script:NC) {1}[{2}]$($script:NC) {3,6} GB used / {4,6} GB free / {5,6} GB total ({6}%)" -f `
|
||||
$_.DeviceID, $color, $bar, $usedGB, $freeGB, $totalGB, $pct)
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
|
||||
function Show-LxsMemoryUsage {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'MEMORY USAGE'
|
||||
Write-Host ''
|
||||
$os = Get-CimInstance Win32_OperatingSystem
|
||||
$totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2)
|
||||
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
|
||||
$usedGB = [math]::Round($totalGB - $freeGB, 2)
|
||||
$pct = if ($totalGB -gt 0) { [math]::Round(100 * $usedGB / $totalGB) } else { 0 }
|
||||
Write-LxsField 'Physical Memory' "$usedGB GB used / $freeGB GB free / $totalGB GB total ($pct%)"
|
||||
|
||||
$pageTotal = [math]::Round($os.SizeStoredInPagingFiles / 1MB, 2)
|
||||
$pageFree = [math]::Round($os.FreeSpaceInPagingFiles / 1MB, 2)
|
||||
Write-LxsField 'Paging File' "$([math]::Round($pageTotal - $pageFree, 2)) GB used / $pageTotal GB total"
|
||||
|
||||
Write-Host "$($script:Cyan)$($script:Bold)Top 10 processes by memory:$($script:NC)"
|
||||
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 10 |
|
||||
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
|
||||
@{ Name = 'Name'; Expression = { $_.ProcessName } },
|
||||
@{ Name = 'Memory (MB)'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } } |
|
||||
Out-String | Write-Host
|
||||
}
|
||||
|
||||
function Show-LxsCpuLoad {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'CPU LOAD'
|
||||
Write-Host ''
|
||||
try {
|
||||
$load = (Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
|
||||
Write-LxsField 'Current Load' "$load%"
|
||||
} catch {
|
||||
Write-LxsWarn 'Could not read the CPU load counter.'
|
||||
}
|
||||
Write-LxsField 'Logical Processors' $env:NUMBER_OF_PROCESSORS
|
||||
|
||||
Write-Host "$($script:Cyan)$($script:Bold)Top 10 processes by CPU time:$($script:NC)"
|
||||
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 |
|
||||
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
|
||||
@{ Name = 'Name'; Expression = { $_.ProcessName } },
|
||||
@{ Name = 'CPU (s)'; Expression = { if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } } } |
|
||||
Out-String | Write-Host
|
||||
}
|
||||
|
||||
function Show-LxsNetwork {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'NETWORK'
|
||||
Write-Host ''
|
||||
try {
|
||||
Get-NetIPConfiguration -ErrorAction Stop | ForEach-Object {
|
||||
Write-Host "$($script:Cyan)$($script:Bold)$($_.InterfaceAlias)$($script:NC) $($script:Gray)($($_.InterfaceDescription))$($script:NC)"
|
||||
Write-Host " IPv4 : $($_.IPv4Address.IPAddress -join ', ')"
|
||||
Write-Host " Gateway : $($_.IPv4DefaultGateway.NextHop -join ', ')"
|
||||
Write-Host " DNS : $($_.DNSServer.ServerAddresses -join ', ')"
|
||||
Write-Host ''
|
||||
}
|
||||
} catch {
|
||||
Write-LxsWarn 'Get-NetIPConfiguration unavailable; falling back to ipconfig.'
|
||||
ipconfig /all | Out-String | Write-Host
|
||||
}
|
||||
Write-LxsField 'Public IP' (Get-LxsPublicIP)
|
||||
}
|
||||
|
||||
function Show-LxsEventLogs {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SYSTEM LOGS' -Right 'LAST 50'
|
||||
Write-Host ''
|
||||
foreach ($logName in @('System', 'Application')) {
|
||||
Write-Host "$($script:Cyan)$($script:Bold)$logName — errors and warnings$($script:NC)"
|
||||
try {
|
||||
$events = Get-WinEvent -FilterHashtable @{ LogName = $logName; Level = 1, 2, 3 } -MaxEvents 25 -ErrorAction Stop
|
||||
$events | Format-Table -AutoSize -Wrap `
|
||||
@{ Name = 'Time'; Expression = { $_.TimeCreated.ToString('MM-dd HH:mm') } },
|
||||
@{ Name = 'Level'; Expression = { $_.LevelDisplayName } },
|
||||
@{ Name = 'Source'; Expression = { $_.ProviderName } },
|
||||
@{ Name = 'Message'; Expression = { ($_.Message -split "`n")[0] } } |
|
||||
Out-String -Width 160 | Write-Host
|
||||
} catch {
|
||||
Write-LxsWarn "No matching entries in $logName (or access denied)."
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsTopProcesses {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'TOP PROCESSES'
|
||||
Write-Host ''
|
||||
Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 20 |
|
||||
Format-Table -AutoSize @{ Name = 'PID'; Expression = { $_.Id } },
|
||||
@{ Name = 'Name'; Expression = { $_.ProcessName } },
|
||||
@{ Name = 'Memory (MB)'; Expression = { [math]::Round($_.WorkingSet64 / 1MB, 1) } },
|
||||
@{ Name = 'CPU (s)'; Expression = { if ($_.CPU) { [math]::Round($_.CPU, 1) } else { 0 } } },
|
||||
@{ Name = 'Threads'; Expression = { $_.Threads.Count } } |
|
||||
Out-String | Write-Host
|
||||
}
|
||||
|
||||
function Show-LxsDiskHealth {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'DISK HEALTH' -Right 'SMART'
|
||||
Write-Host ''
|
||||
try {
|
||||
$disks = @(Get-PhysicalDisk -ErrorAction Stop)
|
||||
} catch {
|
||||
Write-LxsErr 'Get-PhysicalDisk is unavailable on this system.'
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($disk in $disks) {
|
||||
$sizeGB = [math]::Round($disk.Size / 1GB, 1)
|
||||
$healthColor = if ($disk.HealthStatus -eq 'Healthy') { $script:Cyan } else { $script:Red }
|
||||
Write-Host "$($script:White)$($script:Bold)$($disk.FriendlyName)$($script:NC) $($script:Gray)// $($disk.MediaType), $sizeGB GB$($script:NC)"
|
||||
Write-Host " Health : $healthColor$($disk.HealthStatus)$($script:NC) (operational: $($disk.OperationalStatus -join ', '))"
|
||||
|
||||
# Reliability counters need admin; report the gap instead of failing.
|
||||
try {
|
||||
$rc = $disk | Get-StorageReliabilityCounter -ErrorAction Stop
|
||||
if ($null -ne $rc.Temperature) { Write-Host " Temp : $($rc.Temperature) C" }
|
||||
if ($null -ne $rc.Wear) { Write-Host " Wear : $($rc.Wear)%" }
|
||||
if ($null -ne $rc.PowerOnHours) { Write-Host " Power on : $($rc.PowerOnHours) h" }
|
||||
if ($null -ne $rc.ReadErrorsTotal) { Write-Host " Rd errors: $($rc.ReadErrorsTotal)" }
|
||||
if ($null -ne $rc.WriteErrorsTotal) { Write-Host " Wr errors: $($rc.WriteErrorsTotal)" }
|
||||
} catch {
|
||||
if (-not (Test-LxsAdmin)) {
|
||||
Write-Host "$($script:Gray) (run as administrator for SMART counters)$($script:NC)"
|
||||
} else {
|
||||
Write-Host "$($script:Gray) (this device exposes no reliability counters)$($script:NC)"
|
||||
}
|
||||
}
|
||||
Write-Host ''
|
||||
}
|
||||
}
|
||||
|
||||
function Show-LxsSystemInfoMenu {
|
||||
while ($true) {
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'SYSTEM INFOS'
|
||||
Write-Host ''
|
||||
Show-LxsMenuItem '1' 'View system information'
|
||||
Show-LxsMenuItem '2' 'Check disk space'
|
||||
Show-LxsMenuItem '3' 'Check memory usage'
|
||||
Show-LxsMenuItem '4' 'Check CPU load'
|
||||
Show-LxsMenuItem '5' 'Check network'
|
||||
Show-LxsMenuItem '6' 'View system logs (errors and warnings)'
|
||||
Show-LxsMenuItem '7' 'Show top resource-consuming processes'
|
||||
Show-LxsMenuItem '8' 'Check disk health (SMART)'
|
||||
Show-LxsMenuItem '0' 'Back' '' -Exit
|
||||
Write-Host ''
|
||||
Show-LxsBoxBottom
|
||||
Write-Host ''
|
||||
$choice = Read-LxsChoice
|
||||
Write-Host ''
|
||||
|
||||
switch ($choice) {
|
||||
'1' { Show-LxsSystemInformation; Read-LxsEnter }
|
||||
'2' { Show-LxsDiskSpace; Read-LxsEnter }
|
||||
'3' { Show-LxsMemoryUsage; Read-LxsEnter }
|
||||
'4' { Show-LxsCpuLoad; Read-LxsEnter }
|
||||
'5' { Show-LxsNetwork; Read-LxsEnter }
|
||||
'6' { Show-LxsEventLogs; Read-LxsEnter }
|
||||
'7' { Show-LxsTopProcesses; Read-LxsEnter }
|
||||
'8' { Show-LxsDiskHealth; Read-LxsEnter }
|
||||
'0' { return }
|
||||
default { Write-LxsErr 'Invalid protocol. Select 0-8.'; Start-Sleep -Seconds 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
Show-LxsSystemInfoMenu
|
||||
exit 75
|
||||
@@ -0,0 +1,220 @@
|
||||
<#
|
||||
LXS - Update Windows
|
||||
Description: Install pending Windows updates and upgrade every winget
|
||||
package. Mirror of linux/tools/update-server.sh.
|
||||
Repo: https://git.hyko.cx/hykocx/lxs
|
||||
|
||||
Usage: update-windows.ps1 [-Yes] [-NoWinget] [-NoWindowsUpdate] [-Help]
|
||||
#>
|
||||
|
||||
# Load LXS common library (colors, UI helpers, spinner, loggers, guards).
|
||||
# Prefers the sibling ..\lib\common.ps1 (repo checkout or installed layout) and
|
||||
# only hits the network when this script is run standalone.
|
||||
$LxsRawBase = if ($env:LXS_RAW_BASE) { $env:LXS_RAW_BASE } else { 'https://git.hyko.cx/hykocx/lxs/raw/branch/main' }
|
||||
if (-not $env:LXS_RAW_PLATFORM_BASE) { $env:LXS_RAW_PLATFORM_BASE = "$LxsRawBase/windows" }
|
||||
$LxsLibPath = if ($PSScriptRoot) { Join-Path $PSScriptRoot '..\lib\common.ps1' } else { $null }
|
||||
if ($LxsLibPath -and (Test-Path $LxsLibPath)) {
|
||||
. $LxsLibPath
|
||||
} else {
|
||||
try {
|
||||
$LxsLibSource = Invoke-RestMethod -Uri "$env:LXS_RAW_PLATFORM_BASE/lib/common.ps1" -UseBasicParsing -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Error 'Failed to fetch lib/common.ps1'
|
||||
exit 1
|
||||
}
|
||||
. ([scriptblock]::Create($LxsLibSource))
|
||||
}
|
||||
|
||||
$env:LXS_LOG_FILE = Join-Path (Get-LxsTempDir) 'lxs_update_windows.log'
|
||||
|
||||
$LxsOpts = Read-LxsFlags -Arguments $args -Known @(
|
||||
'-Yes', '-y', '-NoWinget', '-NoWindowsUpdate', '-Help', '-h'
|
||||
)
|
||||
if (Show-LxsUnknownFlags $LxsOpts) { exit 1 }
|
||||
$AssumeYes = Test-LxsFlag $LxsOpts @('-Yes', '-y')
|
||||
$NoWinget = Test-LxsFlag $LxsOpts @('-NoWinget')
|
||||
$NoWindowsUpdate = Test-LxsFlag $LxsOpts @('-NoWindowsUpdate')
|
||||
|
||||
if (Test-LxsFlag $LxsOpts @('-Help', '-h')) {
|
||||
Write-Host @'
|
||||
Usage: update-windows.ps1 [options]
|
||||
|
||||
Options:
|
||||
-Yes, -y Skip the confirmation prompt
|
||||
-NoWinget Skip `winget upgrade --all`
|
||||
-NoWindowsUpdate Skip Windows Update
|
||||
-Help, -h Show this help
|
||||
'@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Assert-LxsWindows)) { exit 1 }
|
||||
if (-not $NoWindowsUpdate) { Assert-LxsAdmin -ScriptPath $PSCommandPath -Arguments $args }
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Windows Update via the built-in COM API. Deliberately not PSWindowsUpdate:
|
||||
# that module has to be pulled from the PowerShell Gallery first, which fails
|
||||
# on locked-down or offline machines. The COM surface ships with Windows.
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function Invoke-LxsWindowsUpdate {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'WINDOWS UPDATE'
|
||||
Write-Host ''
|
||||
|
||||
try {
|
||||
$session = New-Object -ComObject Microsoft.Update.Session
|
||||
$searcher = $session.CreateUpdateSearcher()
|
||||
} catch {
|
||||
Write-LxsErr "Windows Update API unavailable: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-LxsInfo 'Searching for updates (this can take several minutes)...'
|
||||
try {
|
||||
$result = $searcher.Search("IsInstalled=0 and Type='Software' and IsHidden=0")
|
||||
} catch {
|
||||
Write-LxsErr "Search failed: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
$updates = @($result.Updates)
|
||||
if ($updates.Count -eq 0) {
|
||||
Write-LxsOk 'No pending updates'
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-LxsOk "$($updates.Count) update(s) available:"
|
||||
Write-Host ''
|
||||
foreach ($u in $updates) {
|
||||
$sizeMB = [math]::Round($u.MaxDownloadSize / 1MB, 1)
|
||||
Write-Host " $($script:Gray)-$($script:NC) $($u.Title) $($script:Gray)(${sizeMB} MB)$($script:NC)"
|
||||
}
|
||||
Write-Host ''
|
||||
|
||||
if (-not (Confirm-LxsAction -Question 'Download and install these updates?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
||||
Write-LxsInfo 'Skipped.'
|
||||
return $true
|
||||
}
|
||||
|
||||
# EULAs must be accepted per-update before an unattended install.
|
||||
$toInstall = New-Object -ComObject Microsoft.Update.UpdateColl
|
||||
foreach ($u in $updates) {
|
||||
if (-not $u.EulaAccepted) {
|
||||
try { $u.AcceptEula() } catch { Write-LxsWarn "Could not accept the EULA for: $($u.Title)" }
|
||||
}
|
||||
[void]$toInstall.Add($u)
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Downloading...'
|
||||
try {
|
||||
$downloader = $session.CreateUpdateDownloader()
|
||||
$downloader.Updates = $toInstall
|
||||
$dlResult = $downloader.Download()
|
||||
if ($dlResult.ResultCode -ne 2) {
|
||||
Write-LxsWarn "Download finished with result code $($dlResult.ResultCode) (2 = success)."
|
||||
} else {
|
||||
Write-LxsOk 'Download complete'
|
||||
}
|
||||
} catch {
|
||||
Write-LxsErr "Download failed: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-LxsInfo 'Installing...'
|
||||
try {
|
||||
$installer = $session.CreateUpdateInstaller()
|
||||
$installer.Updates = $toInstall
|
||||
$instResult = $installer.Install()
|
||||
} catch {
|
||||
Write-LxsErr "Install failed: $($_.Exception.Message)"
|
||||
return $false
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $toInstall.Count; $i++) {
|
||||
$code = $instResult.GetUpdateResult($i).ResultCode
|
||||
$title = $toInstall.Item($i).Title
|
||||
if ($code -eq 2) { Write-LxsOk $title } else { Write-LxsWarn "$title (result code $code)" }
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
if ($instResult.RebootRequired) {
|
||||
Write-LxsWarn 'A reboot is required to finish installing these updates.'
|
||||
} else {
|
||||
Write-LxsOk 'Windows Update finished'
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# winget
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function Invoke-LxsWingetUpgrade {
|
||||
Write-Host ''
|
||||
Show-LxsBoxMid 'WINGET'
|
||||
Write-Host ''
|
||||
|
||||
if (-not (Assert-LxsWinget)) { return $false }
|
||||
|
||||
Write-LxsInfo 'Packages with an available upgrade:'
|
||||
Write-Host ''
|
||||
& winget upgrade --include-unknown --accept-source-agreements | Out-String | Write-Host
|
||||
|
||||
if (-not (Confirm-LxsAction -Question 'Upgrade all of them?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
||||
Write-LxsInfo 'Skipped.'
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
& winget upgrade --all --include-unknown --silent `
|
||||
--accept-package-agreements --accept-source-agreements --disable-interactivity
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
Write-Host ''
|
||||
if ($code -eq 0) {
|
||||
Write-LxsOk 'winget upgrade completed'
|
||||
return $true
|
||||
}
|
||||
# -1978335189 = no applicable upgrade found; nothing actually went wrong.
|
||||
if ($code -eq -1978335189) {
|
||||
Write-LxsOk 'Everything is already up to date'
|
||||
return $true
|
||||
}
|
||||
Write-LxsWarn "winget exited with code $code"
|
||||
return $false
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
# Run
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Clear-Host
|
||||
Show-LxsBoxTop -Title 'UPDATE WINDOWS'
|
||||
Write-Host ''
|
||||
Write-Host 'The following will run on this machine:'
|
||||
if (-not $NoWindowsUpdate) { Write-Host ' - Windows Update: search, download and install pending updates' }
|
||||
if (-not $NoWinget) { Write-Host ' - winget: upgrade every package with a newer version' }
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
|
||||
if (-not (Confirm-LxsAction -Question 'Continue?' -DefaultYes -AssumeYes:$AssumeYes)) {
|
||||
Write-LxsInfo 'Cancelled.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$failed = $false
|
||||
if (-not $NoWindowsUpdate) { if (-not (Invoke-LxsWindowsUpdate)) { $failed = $true } }
|
||||
if (-not $NoWinget) { if (-not (Invoke-LxsWingetUpgrade)) { $failed = $true } }
|
||||
|
||||
Write-Host ''
|
||||
Show-LxsSeparator
|
||||
Write-Host ''
|
||||
if ($failed) {
|
||||
Write-LxsWarn 'Update pass finished with warnings.'
|
||||
exit 1
|
||||
}
|
||||
Write-LxsOk 'Update pass finished'
|
||||
exit 0
|
||||
Reference in New Issue
Block a user