Add fixnetworkshare, winrm-setup-package, udc remote-execution suites

- NetworkDriveManager.ps1: S: drive repair utility
- winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide
- remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating
  DNC controllers on shop-floor PCs
- Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines)
- Schedule-Maintenance and complete-asset minor updates
- Bump edncfix gitlink to v1.6.0 (2748bfa)
- .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-04-17 12:04:40 -04:00
parent 847ec402bd
commit 86b32d8597
24 changed files with 6945 additions and 1352 deletions

View File

@@ -0,0 +1,535 @@
<#
.SYNOPSIS
Simple remote maintenance toolkit for Windows PCs via WinRM.
.DESCRIPTION
Executes maintenance tasks on remote Windows PCs using WinRM.
Reads target computers from a text file (one hostname/IP per line).
.PARAMETER HostsFile
Path to text file containing computer names/IPs (one per line).
Lines starting with # are treated as comments.
Default: .\hosts.txt
.PARAMETER ComputerName
Single computer name or IP address (alternative to HostsFile).
.PARAMETER Task
Maintenance task to execute. Available tasks:
- RestartSpooler : Restart Print Spooler service
- FlushDNS : Clear DNS resolver cache
- ClearTempFiles : Clear Windows temp files
- DiskCleanup : Run Windows Disk Cleanup
- OptimizeDisk : TRIM (SSD) or Defrag (HDD)
- SyncTime : Force time sync with domain controller
- RestartService : Restart a specific Windows service
- RunCommand : Run a custom command
- RestartComputer : Restart the remote PC (requires confirmation)
.PARAMETER ServiceName
Service name for RestartService task.
.PARAMETER Command
Custom command for RunCommand task.
.PARAMETER Credential
PSCredential for remote authentication. Prompts if not provided.
.PARAMETER DnsSuffix
DNS suffix to append to hostnames (if not already FQDN).
Default: logon.ds.ge.com
.PARAMETER ThrottleLimit
Maximum number of concurrent remote connections.
Default: 10
.PARAMETER LogResults
Save results to a timestamped log file in the script directory.
Log files are saved as: RemoteTask_YYYYMMDD_HHMMSS.log
.EXAMPLE
# Restart print spooler on all hosts in hosts.txt
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
.EXAMPLE
# Flush DNS on a single computer
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS
.EXAMPLE
# Run disk cleanup on hosts from custom file
.\Invoke-RemoteTask.ps1 -HostsFile ".\shopfloor-pcs.txt" -Task DiskCleanup
.EXAMPLE
# Restart a specific service
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler"
.EXAMPLE
# Run custom command
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Select -First 5"
.NOTES
Author: Shop Floor Tools
Requirements: PowerShell 5.1+, WinRM enabled on targets
#>
[CmdletBinding(DefaultParameterSetName='ByFile')]
param(
[Parameter(ParameterSetName='ByFile')]
[string]$HostsFile = ".\hosts.txt",
[Parameter(ParameterSetName='ByName')]
[string[]]$ComputerName,
[Parameter(Mandatory=$true)]
[ValidateSet(
'RestartSpooler', 'FlushDNS', 'ClearTempFiles', 'DiskCleanup',
'OptimizeDisk', 'SyncTime', 'RestartService', 'RunCommand',
'GetDiskSpace', 'GetUptime', 'TestConnection', 'RestartComputer'
)]
[string]$Task,
[Parameter()]
[string]$ServiceName,
[Parameter()]
[string]$Command,
[Parameter()]
[PSCredential]$Credential,
[Parameter()]
[string]$DnsSuffix = "logon.ds.ge.com",
[Parameter()]
[int]$ThrottleLimit = 10,
[Parameter()]
[switch]$LogResults
)
# =============================================================================
# Helper Functions
# =============================================================================
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$color = switch ($Level) {
"ERROR" { "Red" }
"WARNING" { "Yellow" }
"SUCCESS" { "Green" }
"TASK" { "Cyan" }
default { "White" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
# =============================================================================
# Task Scriptblocks
# =============================================================================
$TaskScripts = @{
'RestartSpooler' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
Stop-Service -Name Spooler -Force -ErrorAction Stop
$queuePath = "$env:SystemRoot\System32\spool\PRINTERS"
if (Test-Path $queuePath) { Remove-Item "$queuePath\*" -Force -ErrorAction SilentlyContinue }
Start-Service -Name Spooler -ErrorAction Stop
$status = (Get-Service -Name Spooler).Status
$result.Success = ($status -eq 'Running')
$result.Output = "Print Spooler restarted. Status: $status"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'FlushDNS' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$flushResult = & ipconfig /flushdns 2>&1
$result.Output = ($flushResult -join " ").Trim()
$result.Success = $true
} catch { $result.Error = $_.Exception.Message }
return $result
}
'ClearTempFiles' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; FilesDeleted = 0 }
try {
$tempPaths = @("$env:TEMP", "$env:SystemRoot\Temp")
foreach ($path in $tempPaths) {
if (Test-Path $path) {
$files = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue
foreach ($file in $files) {
try { Remove-Item $file.FullName -Force -Recurse -ErrorAction SilentlyContinue; $result.FilesDeleted++ } catch { }
}
}
}
$result.Success = $true
$result.Output = "Deleted $($result.FilesDeleted) temp files"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'DiskCleanup' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; SpaceFreedMB = 0 }
try {
$initialFree = (Get-PSDrive C).Free
$cleanupPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
$categories = @("Temporary Files", "Temporary Setup Files", "Old ChkDsk Files", "Windows Update Cleanup", "Recycle Bin")
foreach ($cat in $categories) {
$catPath = Join-Path $cleanupPath $cat
if (Test-Path $catPath) { Set-ItemProperty -Path $catPath -Name "StateFlags0100" -Value 2 -ErrorAction SilentlyContinue }
}
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:100" -Wait -WindowStyle Hidden
Start-Sleep -Seconds 2
$finalFree = (Get-PSDrive C).Free
$result.SpaceFreedMB = [math]::Round(($finalFree - $initialFree) / 1MB, 0)
$result.Success = $true
$result.Output = "Disk cleanup completed. Space freed: $($result.SpaceFreedMB) MB"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'OptimizeDisk' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.DriveLetter }
$optimized = @()
foreach ($vol in $volumes) {
$driveLetter = $vol.DriveLetter
$physicalDisk = Get-PhysicalDisk | Where-Object { $_.DeviceId -eq (Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue).DiskNumber }
$mediaType = if ($physicalDisk) { $physicalDisk.MediaType } else { "Unknown" }
try {
if ($mediaType -eq 'SSD') { Optimize-Volume -DriveLetter $driveLetter -ReTrim -ErrorAction Stop; $action = "TRIM" }
else { Optimize-Volume -DriveLetter $driveLetter -Defrag -ErrorAction Stop; $action = "Defrag" }
$optimized += "${driveLetter}:($action)"
} catch { $optimized += "${driveLetter}:(Failed)" }
}
$result.Success = $true
$result.Output = "Optimized: $($optimized -join ', ')"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'SyncTime' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$source = (& w32tm /query /source 2>&1) -join " "
$syncResult = & w32tm /resync /force 2>&1
$result.Success = ($syncResult -match "success" -or $LASTEXITCODE -eq 0)
$result.Output = "Time synced with $source. Current: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'RestartService' = {
param($ServiceName)
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
if (-not $ServiceName) { $result.Error = "ServiceName parameter required"; return $result }
try {
Restart-Service -Name $ServiceName -Force -ErrorAction Stop
$status = (Get-Service -Name $ServiceName).Status
$result.Success = ($status -eq 'Running')
$result.Output = "Service '$ServiceName' restarted. Status: $status"
} catch { $result.Error = $_.Exception.Message }
return $result
}
'RunCommand' = {
param($Command)
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
if (-not $Command) { $result.Error = "Command parameter required"; return $result }
try {
$output = Invoke-Expression $Command 2>&1
$result.Output = ($output | Out-String).Trim()
$result.Success = $true
} catch { $result.Error = $_.Exception.Message }
return $result
}
'GetDiskSpace' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -ne $null }
$info = foreach ($drive in $drives) {
$freeGB = [math]::Round($drive.Free / 1GB, 1)
$usedGB = [math]::Round($drive.Used / 1GB, 1)
$totalGB = $freeGB + $usedGB
$pctFree = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 0) } else { 0 }
"$($drive.Name): $freeGB GB free ($pctFree%)"
}
$result.Output = $info -join ", "
$result.Success = $true
} catch { $result.Error = $_.Exception.Message }
return $result
}
'GetUptime' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
$result.Output = "Up $([math]::Floor($uptime.TotalDays))d $($uptime.Hours)h $($uptime.Minutes)m (Last boot: $($os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm')))"
$result.Success = $true
} catch { $result.Error = $_.Exception.Message }
return $result
}
'TestConnection' = {
$result = @{ Success = $true; Hostname = $env:COMPUTERNAME; Output = "Connection successful"; Error = $null }
return $result
}
'RestartComputer' = {
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
try {
$result.Output = "Restart initiated"
$result.Success = $true
# Schedule restart in 5 seconds to allow response to return
Start-Process -FilePath "shutdown.exe" -ArgumentList "/r /t 5 /c `"Remote restart initiated via WinRM`"" -NoNewWindow
} catch { $result.Error = $_.Exception.Message }
return $result
}
}
# =============================================================================
# Main Execution
# =============================================================================
Write-Host ""
Write-Host ("=" * 60) -ForegroundColor Cyan
Write-Host " Remote Task Executor - Task: $Task" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor Cyan
Write-Host ""
# Validate task-specific parameters
if ($Task -eq 'RestartService' -and -not $ServiceName) {
Write-Log "ServiceName parameter is required for RestartService task" -Level "ERROR"
exit 1
}
if ($Task -eq 'RunCommand' -and -not $Command) {
Write-Log "Command parameter is required for RunCommand task" -Level "ERROR"
exit 1
}
if ($Task -eq 'RestartComputer') {
Write-Host ""
Write-Host "WARNING: This will restart the target computer(s)!" -ForegroundColor Yellow
$confirm = Read-Host "Type 'YES' to confirm"
if ($confirm -ne 'YES') {
Write-Log "Restart cancelled by user" -Level "WARNING"
exit 0
}
}
# Get credentials
if (-not $Credential) {
Write-Log "Enter credentials for remote PCs:" -Level "INFO"
$Credential = Get-Credential -Message "Enter admin credentials for remote PCs"
if (-not $Credential) {
Write-Log "Credentials required. Exiting." -Level "ERROR"
exit 1
}
}
# Build computer list
$computers = @()
if ($ComputerName) {
$computers = $ComputerName
} else {
if (-not (Test-Path $HostsFile)) {
Write-Log "Hosts file not found: $HostsFile" -Level "ERROR"
Write-Log "Create a text file with one hostname or IP per line." -Level "INFO"
exit 1
}
$computers = Get-Content $HostsFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") }
}
if ($computers.Count -eq 0) {
Write-Log "No computers specified." -Level "ERROR"
exit 1
}
Write-Log "Target computers: $($computers.Count)" -Level "INFO"
Write-Host ""
# Build FQDNs if DNS suffix provided
$targets = $computers | ForEach-Object {
$name = $_.Trim()
if ($DnsSuffix -and $name -notlike "*.*") {
"$name.$DnsSuffix"
} else {
$name
}
}
# Get the scriptblock
$scriptBlock = $TaskScripts[$Task]
# Create session options
$sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 300000 -NoMachineProfile
Write-Log "Executing on $($targets.Count) computer(s) in parallel (ThrottleLimit: $ThrottleLimit)..." -Level "INFO"
Write-Host ""
# Build arguments for tasks that need them
$taskArgs = @()
if ($Task -eq 'RestartService') { $taskArgs = @($ServiceName) }
if ($Task -eq 'RunCommand') { $taskArgs = @($Command) }
# Show progress indicator
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
Write-Host " [" -NoNewline
Write-Host "Running..." -ForegroundColor Yellow -NoNewline
Write-Host "] Please wait..." -NoNewline
# Execute on all remote computers in parallel using Invoke-Command
$results = @()
$connectionErrors = @()
try {
if ($taskArgs.Count -gt 0) {
$results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock -ArgumentList $taskArgs `
-Credential $Credential -SessionOption $sessionOption -Authentication Negotiate `
-ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors
} else {
$results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock `
-Credential $Credential -SessionOption $sessionOption -Authentication Negotiate `
-ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors
}
} catch {
Write-Host ""
Write-Log "Execution error: $($_.Exception.Message)" -Level "ERROR"
}
$stopwatch.Stop()
$elapsed = $stopwatch.Elapsed.TotalSeconds
# Clear the progress line
Write-Host "`r" -NoNewline
Write-Host (" " * 60) -NoNewline
Write-Host "`r" -NoNewline
Write-Log "Completed in $([math]::Round($elapsed, 1)) seconds" -Level "INFO"
Write-Host ""
# Collect all results (successes, failures, connection errors)
$allResults = @()
$successCount = 0
$failCount = 0
foreach ($result in $results) {
$status = if ($result.Success) { "OK"; $successCount++ } else { "FAIL"; $failCount++ }
$message = if ($result.Success) { $result.Output } else { $result.Error }
$allResults += [PSCustomObject]@{
Status = $status
Computer = $result.Hostname
Message = $message
}
}
# Process connection errors
foreach ($err in $connectionErrors) {
$targetName = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" }
# Extract just the computer name from FQDN for display
$shortName = ($targetName -split '\.')[0]
$errorMsg = $err.Exception.Message -replace '\r?\n', ' '
# Truncate long error messages
if ($errorMsg.Length -gt 60) { $errorMsg = $errorMsg.Substring(0, 57) + "..." }
$allResults += [PSCustomObject]@{
Status = "FAIL"
Computer = $shortName
Message = $errorMsg
}
$failCount++
}
# Sort results: failures first, then successes
$allResults = $allResults | Sort-Object @{Expression={$_.Status}; Descending=$true}, Computer
# Display results in a formatted table
Write-Host " STATUS COMPUTER RESULT" -ForegroundColor Cyan
Write-Host " ------ -------- ------" -ForegroundColor Cyan
foreach ($r in $allResults) {
$statusColor = if ($r.Status -eq "OK") { "Green" } else { "Red" }
$statusIcon = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL]" }
# Pad/truncate computer name to 20 chars
$compName = $r.Computer
if ($compName.Length -gt 18) { $compName = $compName.Substring(0, 15) + "..." }
$compName = $compName.PadRight(20)
# Truncate message if too long
$msg = $r.Message
if ($msg.Length -gt 50) { $msg = $msg.Substring(0, 47) + "..." }
Write-Host " " -NoNewline
Write-Host $statusIcon -ForegroundColor $statusColor -NoNewline
Write-Host " $compName " -NoNewline
Write-Host $msg -ForegroundColor $(if ($r.Status -eq "OK") { "White" } else { "Yellow" })
}
Write-Host ""
# Summary
Write-Host ""
Write-Host ("=" * 60) -ForegroundColor Cyan
Write-Host " SUMMARY" -ForegroundColor Cyan
Write-Host ("=" * 60) -ForegroundColor Cyan
Write-Host " Task: $Task" -ForegroundColor White
Write-Host " Total: $($computers.Count)" -ForegroundColor White
Write-Host " Successful: $successCount" -ForegroundColor Green
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
Write-Host ("=" * 60) -ForegroundColor Cyan
Write-Host ""
# Save to log file if requested
if ($LogResults) {
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if (-not $scriptDir) { $scriptDir = Get-Location }
$logTimestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$logFile = Join-Path $scriptDir "RemoteTask_$logTimestamp.log"
$logContent = @()
$logContent += "=" * 60
$logContent += "Remote Task Execution Log"
$logContent += "=" * 60
$logContent += "Date/Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$logContent += "Task: $Task"
$logContent += "Targets: $($computers.Count)"
$logContent += "ThrottleLimit: $ThrottleLimit"
$logContent += "Elapsed: $([math]::Round($elapsed, 1)) seconds"
if ($ServiceName) { $logContent += "ServiceName: $ServiceName" }
if ($Command) { $logContent += "Command: $Command" }
$logContent += ""
$logContent += "=" * 60
$logContent += "RESULTS"
$logContent += "=" * 60
$logContent += ""
$logContent += "STATUS COMPUTER RESULT"
$logContent += "------ -------- ------"
foreach ($r in $allResults) {
$statusText = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL] " }
$compText = $r.Computer.PadRight(28)
$logContent += "$statusText $compText $($r.Message)"
}
$logContent += ""
$logContent += "=" * 60
$logContent += "SUMMARY"
$logContent += "=" * 60
$logContent += "Total: $($computers.Count)"
$logContent += "Successful: $successCount"
$logContent += "Failed: $failCount"
$logContent += "=" * 60
$logContent | Out-File -FilePath $logFile -Encoding UTF8
Write-Log "Results saved to: $logFile" -Level "SUCCESS"
Write-Host ""
}
# Results are displayed above and optionally saved to log file
# To capture results programmatically, use: $results = Invoke-Command ... directly

View File

@@ -0,0 +1,296 @@
# WinRM Setup Package for Shopfloor PCs
This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks.
## Contents
| File | Description |
|------|-------------|
| `Setup-WinRM.bat` | Run on each PC to enable and configure WinRM |
| `Invoke-RemoteTask.ps1` | PowerShell script to execute tasks on remote PCs |
| `hosts.txt` | List of target computers (edit before use) |
| `README.md` | This documentation |
---
## Quick Start
### Step 1: Configure Your Admin Workstation
Before connecting to remote PCs, run this once on your admin workstation (as Administrator):
```powershell
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
```
This allows your workstation to connect to any PC in the domain.
### Step 2: Configure the Setup Script
Edit `Setup-WinRM.bat` and update these values at the top:
```batch
REM Default security group - who can use WinRM to connect
set "DEFAULT_SECURITY_GROUP=logon\groupid"
REM Where to log the inventory CSV (network share recommended)
set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory"
REM Domain suffix for TrustedHosts
set "TRUSTED_DOMAIN=*.logon.ds.ge.com"
REM Optional: Trust a specific subnet (uncomment and set)
REM set "TRUSTED_SUBNET=10.48.130.*"
```
### Step 3: Create Security Group in Active Directory
1. Open **Active Directory Users and Computers**
2. Create a new Security Group (or use existing group matching `groupid`)
3. Add users who should have remote management access
### Step 4: Run Setup on Each Shopfloor PC
Run as Administrator on each PC:
```cmd
Setup-WinRM.bat
```
Or with parameters:
```cmd
Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"
```
The script will:
- Enable WinRM service
- Configure authentication (Negotiate/Kerberos)
- Set firewall rules (domain profile only)
- Restrict access to the security group
- Log hostname/IP to CSV inventory
### Step 5: Run Remote Tasks
From your admin workstation, edit `hosts.txt` with target PCs, then:
```powershell
# Test connectivity
.\Invoke-RemoteTask.ps1 -Task TestConnection
# Restart print spooler on all hosts
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
# Check disk space
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace
# Run on a single PC
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS
```
---
## Setup Script Details
### What Setup-WinRM.bat Configures
| Setting | Value | Purpose |
|---------|-------|---------|
| WinRM Service | Auto-start | Ensures WinRM starts on boot |
| AllowUnencrypted | false | Security: require encrypted connections |
| Negotiate Auth | true | Enables Kerberos/NTLM authentication |
| CredSSP Auth | true | Enables credential delegation (double-hop) |
| Firewall | Domain profile | Opens port 5985 for domain connections only |
| TrustedHosts | *.logon.ds.ge.com | Trusts domain-joined PCs |
| RootSDDL | Security group | Restricts who can connect |
### CSV Inventory
The setup script logs each PC to a CSV file:
```csv
Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup
PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid
PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid
```
You can use this CSV as your hosts file:
```powershell
# Extract hostnames from CSV
Import-Csv "\\server\share\winrm-inventory\winrm-inventory.csv" |
Select-Object -ExpandProperty Hostname |
Set-Content .\hosts.txt
```
---
## Available Remote Tasks
| Task | Description |
|------|-------------|
| `TestConnection` | Verify WinRM connectivity |
| `GetUptime` | Show system uptime and last boot time |
| `GetDiskSpace` | Show free space on all drives |
| `RestartSpooler` | Restart Print Spooler service |
| `FlushDNS` | Clear DNS resolver cache |
| `ClearTempFiles` | Delete Windows temp files |
| `DiskCleanup` | Run Windows Disk Cleanup |
| `OptimizeDisk` | TRIM (SSD) or Defrag (HDD) |
| `SyncTime` | Force time sync with domain controller |
| `RestartService` | Restart any Windows service (requires `-ServiceName`) |
| `RunCommand` | Run custom PowerShell command (requires `-Command`) |
| `RestartComputer` | Restart the remote PC (requires YES confirmation) |
### Examples
```powershell
# Check uptime on all hosts
.\Invoke-RemoteTask.ps1 -Task GetUptime
# Restart a specific service
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler"
# Run custom command
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Sort CPU -Desc | Select -First 5"
# Use custom hosts file
.\Invoke-RemoteTask.ps1 -HostsFile ".\cnc-machines.txt" -Task FlushDNS
# Specify DNS suffix for short hostnames
.\Invoke-RemoteTask.ps1 -DnsSuffix "logon.ds.ge.com" -Task TestConnection
# Restart a remote PC (will prompt for confirmation)
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task RestartComputer
# Increase parallelism for faster execution on many PCs
.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20
# Save results to a log file
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults
```
### Logging Results
Use `-LogResults` to save task output to a timestamped log file in the script directory:
```powershell
.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults
# Creates: RemoteTask_20260108_143022.log
```
Log files contain:
- Task name and parameters
- Execution time
- Status of each computer (OK/FAIL)
- Result messages
- Summary totals
### Targeting Multiple PCs
```powershell
# Comma-separated list
.\Invoke-RemoteTask.ps1 -ComputerName "PC001","PC002","PC003" -Task GetUptime
# Array variable
$pcs = @("PC001", "PC002", "PC003")
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS
# From hosts.txt file (default)
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
# From CSV inventory
$pcs = (Import-Csv "\\server\share\winrm-inventory.csv").Hostname
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace
# From Active Directory query
$pcs = (Get-ADComputer -Filter "Name -like 'SHOPFLOOR-*'").Name
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime
```
All commands run in parallel (default: 10 concurrent connections, adjust with `-ThrottleLimit`).
---
## Troubleshooting
### "Access Denied" when connecting
1. Verify you're a member of the WinRM security group
2. Check that your credentials are correct
3. Verify the target PC ran Setup-WinRM.bat successfully
### "WinRM cannot complete the operation"
1. Verify the target PC is reachable: `ping PC001`
2. Check WinRM is running on target: `sc query winrm` (on target PC)
3. Verify firewall allows port 5985
### "The WinRM client cannot process the request"
1. Add target to TrustedHosts on your admin workstation:
```powershell
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
```
### Test WinRM Configuration
On your admin workstation:
```powershell
# Test basic connectivity
Test-WSMan -ComputerName PC001
# Test with credentials
$cred = Get-Credential
Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate
# Enter interactive session
Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate
```
On the target PC:
```cmd
winrm enumerate winrm/config/listener
winrm get winrm/config/service
```
---
## Security Considerations
1. **Use Security Groups**: Always restrict WinRM access to a specific AD group
2. **Domain Profile Only**: Firewall rules only allow connections on domain networks
3. **No Unencrypted Traffic**: AllowUnencrypted is set to false
4. **Audit Access**: Enable Windows Security auditing for logon events
5. **Credential Protection**: Use dedicated admin accounts, not personal accounts
---
## Adding Custom Tasks
Edit `Invoke-RemoteTask.ps1` and add to the `$TaskScripts` hashtable:
```powershell
'MyCustomTask' = {
$result = @{
Success = $false
Hostname = $env:COMPUTERNAME
Output = ""
Error = $null
}
try {
# Your code here
$result.Output = "Task completed"
$result.Success = $true
} catch {
$result.Error = $_.Exception.Message
}
return $result
}
```
Then add the task name to the `ValidateSet` in the param block.
---
## Support
For issues or questions, contact your IT support team.

View File

@@ -0,0 +1,269 @@
@echo off
REM ============================================================================
REM WinRM Setup Script for Shopfloor PCs
REM ============================================================================
REM
REM PURPOSE: Configures WinRM on a Windows PC and restricts access to members
REM of a specific Active Directory security group. Logs setup to CSV.
REM
REM USAGE: Run as Administrator on each shopfloor PC
REM Setup-WinRM.bat [SecurityGroupName] [LogPath]
REM
REM EXAMPLE: Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"
REM
REM REQUIREMENTS:
REM - Must be run as Administrator
REM - PC must be domain-joined
REM - Security group must exist in Active Directory
REM
REM ============================================================================
setlocal EnableDelayedExpansion
REM Check for admin privileges
net session >nul 2>&1
if %ERRORLEVEL% neq 0 (
echo.
echo ERROR: This script must be run as Administrator.
echo Right-click and select "Run as administrator"
echo.
pause
exit /b 1
)
REM ============================================================================
REM Configuration - EDIT THESE VALUES FOR YOUR ENVIRONMENT
REM ============================================================================
REM Default security group (can be overridden by parameter)
set "DEFAULT_SECURITY_GROUP=logon\groupid"
REM Default log path for CSV inventory (can be overridden by parameter)
REM Use a network share that all PCs can write to
set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory"
REM Domain suffix for TrustedHosts (e.g., *.logon.ds.ge.com)
set "TRUSTED_DOMAIN=*.logon.ds.ge.com"
REM Optional: Trusted subnets - comma-separated (leave empty to skip)
REM For /24 subnet: "10.48.130.*"
REM For /23 subnet: "10.48.130.*,10.48.131.*"
REM For /22 subnet: "10.48.128.*,10.48.129.*,10.48.130.*,10.48.131.*"
REM set "TRUSTED_SUBNET=10.48.130.*,10.48.131.*"
set "TRUSTED_SUBNET="
REM ============================================================================
REM Get parameters or use defaults
set "SECURITY_GROUP=%~1"
set "LOG_PATH=%~2"
if "%SECURITY_GROUP%"=="" set "SECURITY_GROUP=%DEFAULT_SECURITY_GROUP%"
if "%LOG_PATH%"=="" set "LOG_PATH=%DEFAULT_LOG_PATH%"
echo.
echo ============================================================================
echo WinRM Setup Script
echo ============================================================================
echo.
echo Computer: %COMPUTERNAME%
echo Security Group: %SECURITY_GROUP%
echo Log Path: %LOG_PATH%
echo Trusted Domain: %TRUSTED_DOMAIN%
if not "%TRUSTED_SUBNET%"=="" echo Trusted Subnet: %TRUSTED_SUBNET%
echo.
echo ============================================================================
echo.
REM Step 1: Enable WinRM service
echo [1/7] Enabling WinRM service...
sc config WinRM start= auto >nul 2>&1
net start WinRM >nul 2>&1
if %ERRORLEVEL% equ 0 (
echo WinRM service started
) else (
echo WinRM service already running
)
REM Step 2: Run quick config (creates listener, firewall rules)
echo [2/7] Running WinRM quick configuration...
winrm quickconfig -quiet >nul 2>&1
echo Quick config completed
REM Step 3: Configure WinRM settings
echo [3/7] Configuring WinRM settings...
REM Disable unencrypted traffic (security best practice)
winrm set winrm/config/service @{AllowUnencrypted="false"} >nul 2>&1
REM Enable Negotiate authentication (Kerberos/NTLM)
winrm set winrm/config/service/auth @{Negotiate="true"} >nul 2>&1
REM Enable CredSSP for double-hop scenarios (optional)
winrm set winrm/config/service/auth @{CredSSP="true"} >nul 2>&1
REM Set max concurrent operations
winrm set winrm/config/service @{MaxConcurrentOperationsPerUser="50"} >nul 2>&1
REM Set max memory per shell (512MB)
winrm set winrm/config/winrs @{MaxMemoryPerShellMB="512"} >nul 2>&1
echo WinRM settings configured
REM Step 4: Configure TrustedHosts on CLIENT side (for the admin workstation)
REM This step configures this PC to trust connections TO other PCs
echo [4/7] Configuring TrustedHosts...
REM Build TrustedHosts value
set "TRUSTED_HOSTS=%TRUSTED_DOMAIN%"
if not "%TRUSTED_SUBNET%"=="" (
set "TRUSTED_HOSTS=%TRUSTED_HOSTS%,%TRUSTED_SUBNET%"
)
REM Get current TrustedHosts and append if needed
powershell -ExecutionPolicy Bypass -Command ^
"$currentTrusted = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction SilentlyContinue).Value; " ^
"$newHosts = '%TRUSTED_HOSTS%'; " ^
"if ([string]::IsNullOrEmpty($currentTrusted)) { " ^
" Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force; " ^
" Write-Host ' Set TrustedHosts: ' $newHosts; " ^
"} elseif ($currentTrusted -notlike '*%TRUSTED_DOMAIN%*') { " ^
" $combined = $currentTrusted + ',' + $newHosts; " ^
" Set-Item WSMan:\localhost\Client\TrustedHosts -Value $combined -Force; " ^
" Write-Host ' Added to TrustedHosts: ' $newHosts; " ^
"} else { " ^
" Write-Host ' TrustedHosts already configured'; " ^
"}"
REM Step 5: Configure firewall rules
echo [5/7] Configuring firewall rules...
REM Enable WinRM firewall rule for domain profile
netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes profile=domain >nul 2>&1
if %ERRORLEVEL% neq 0 (
netsh advfirewall firewall add rule name="Windows Remote Management (HTTP-In)" dir=in action=allow protocol=tcp localport=5985 profile=domain >nul 2>&1
)
echo Firewall rule enabled for domain profile
REM Step 6: Set WinRM permissions for security group
echo [6/7] Configuring WinRM permissions for security group...
powershell -ExecutionPolicy Bypass -Command ^
"$group = '%SECURITY_GROUP%'; " ^
"try { " ^
" $ntAccount = New-Object System.Security.Principal.NTAccount($group); " ^
" $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]); " ^
" $sidString = $sid.Value; " ^
" Write-Host ' Group SID: ' $sidString; " ^
" $currentSDDL = (Get-Item WSMan:\localhost\Service\RootSDDL).Value; " ^
" $newACE = '(A;;GXGR;;;' + $sidString + ')'; " ^
" if ($currentSDDL -notmatch [regex]::Escape($sidString)) { " ^
" $newSDDL = $currentSDDL -replace 'D:', ('D:' + $newACE); " ^
" Set-Item WSMan:\localhost\Service\RootSDDL -Value $newSDDL -Force; " ^
" Write-Host ' Added security group to WinRM permissions'; " ^
" } else { " ^
" Write-Host ' Security group already has WinRM permissions'; " ^
" } " ^
"} catch { " ^
" Write-Host ' ERROR: Could not resolve security group - ' $_.Exception.Message; " ^
" exit 1; " ^
"}"
if %ERRORLEVEL% neq 0 (
echo.
echo ERROR: Failed to configure security group permissions.
echo Verify the security group exists in Active Directory.
echo.
pause
exit /b 1
)
REM Step 7: Log to CSV inventory file
echo [7/7] Logging to inventory CSV...
REM Get IP address
for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /i "IPv4"') do (
set "IP_ADDRESS=%%a"
goto :gotip
)
:gotip
set "IP_ADDRESS=%IP_ADDRESS: =%"
REM Get current date/time
for /f "tokens=2 delims==" %%a in ('wmic os get localdatetime /value') do set "DT=%%a"
set "SETUP_DATE=%DT:~0,4%-%DT:~4,2%-%DT:~6,2% %DT:~8,2%:%DT:~10,2%:%DT:~12,2%"
REM Get OS version
for /f "tokens=4-5 delims=. " %%a in ('ver') do set "OS_VERSION=%%a.%%b"
REM Create CSV directory if it doesn't exist
if not exist "%LOG_PATH%" (
mkdir "%LOG_PATH%" 2>nul
if %ERRORLEVEL% neq 0 (
echo WARNING: Could not create log directory. Logging skipped.
goto :skiplog
)
)
REM Define CSV file
set "CSV_FILE=%LOG_PATH%\winrm-inventory.csv"
REM Create CSV header if file doesn't exist
if not exist "%CSV_FILE%" (
echo Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup > "%CSV_FILE%"
if %ERRORLEVEL% neq 0 (
echo WARNING: Could not create CSV file. Logging skipped.
goto :skiplog
)
)
REM Check if this hostname already exists in CSV and update or append
powershell -ExecutionPolicy Bypass -Command ^
"$csvFile = '%CSV_FILE%'; " ^
"$hostname = '%COMPUTERNAME%'; " ^
"$newLine = '%COMPUTERNAME%,%IP_ADDRESS%,%SETUP_DATE%,%OS_VERSION%,%SECURITY_GROUP%'; " ^
"try { " ^
" $content = Get-Content $csvFile -ErrorAction SilentlyContinue; " ^
" $found = $false; " ^
" $newContent = @(); " ^
" foreach ($line in $content) { " ^
" if ($line -like \"$hostname,*\") { " ^
" $newContent += $newLine; " ^
" $found = $true; " ^
" } else { " ^
" $newContent += $line; " ^
" } " ^
" } " ^
" if (-not $found) { $newContent += $newLine; } " ^
" $newContent | Set-Content $csvFile -Force; " ^
" Write-Host ' Logged: %COMPUTERNAME% (%IP_ADDRESS%)'; " ^
"} catch { " ^
" Write-Host ' WARNING: Could not write to CSV - ' $_.Exception.Message; " ^
"}"
:skiplog
REM Verify configuration
echo.
echo ============================================================================
echo WinRM Setup Complete!
echo ============================================================================
echo.
echo Computer: %COMPUTERNAME%
echo IP Address: %IP_ADDRESS%
echo Security Group: %SECURITY_GROUP%
echo WinRM Port: 5985 (HTTP)
echo Trusted Hosts: %TRUSTED_HOSTS%
echo.
echo Inventory logged to: %CSV_FILE%
echo.
echo Members of '%SECURITY_GROUP%' can now connect using:
echo Enter-PSSession -ComputerName %COMPUTERNAME% -Credential (Get-Credential)
echo.
echo To test from a remote PC (as a member of the security group):
echo Test-WSMan -ComputerName %COMPUTERNAME%
echo.
echo ============================================================================
pause
exit /b 0

View File

@@ -0,0 +1,424 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WinRM Setup Package for Shopfloor PCs</title>
<style>
* { box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
color: #333;
}
h1 {
color: #0078d4;
border-bottom: 3px solid #0078d4;
padding-bottom: 10px;
}
h2 {
color: #106ebe;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
margin-top: 30px;
}
h3 {
color: #333;
margin-top: 25px;
}
code {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.9em;
}
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 0.85em;
line-height: 1.4;
}
pre code {
background: none;
padding: 0;
color: inherit;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
th, td {
padding: 10px 12px;
text-align: left;
border: 1px solid #ddd;
}
th {
background: #0078d4;
color: white;
font-weight: 600;
}
tr:nth-child(even) { background: #f9f9f9; }
tr:hover { background: #f0f7ff; }
hr {
border: none;
border-top: 1px solid #ddd;
margin: 30px 0;
}
.warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 10px 15px;
margin: 15px 0;
}
.info {
background: #e7f3ff;
border-left: 4px solid #0078d4;
padding: 10px 15px;
margin: 15px 0;
}
ol, ul { padding-left: 25px; }
li { margin: 5px 0; }
.toc {
background: white;
padding: 15px 20px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.toc h3 { margin-top: 0; }
.toc ul { list-style: none; padding-left: 0; }
.toc li { margin: 8px 0; }
.toc a { color: #0078d4; text-decoration: none; }
.toc a:hover { text-decoration: underline; }
.comment { color: #6a9955; }
.string { color: #ce9178; }
.keyword { color: #569cd6; }
</style>
</head>
<body>
<h1>WinRM Setup Package for Shopfloor PCs</h1>
<p>This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks.</p>
<div class="toc">
<h3>Contents</h3>
<ul>
<li><a href="#quick-start">Quick Start</a></li>
<li><a href="#setup-details">Setup Script Details</a></li>
<li><a href="#tasks">Available Remote Tasks</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
<li><a href="#security">Security Considerations</a></li>
<li><a href="#custom-tasks">Adding Custom Tasks</a></li>
</ul>
</div>
<h2>Package Contents</h2>
<table>
<tr><th>File</th><th>Description</th></tr>
<tr><td><code>Setup-WinRM.bat</code></td><td>Run on each PC to enable and configure WinRM</td></tr>
<tr><td><code>Invoke-RemoteTask.ps1</code></td><td>PowerShell script to execute tasks on remote PCs</td></tr>
<tr><td><code>hosts.txt</code></td><td>List of target computers (edit before use)</td></tr>
<tr><td><code>WinRM-Setup-Guide.html</code></td><td>This documentation</td></tr>
</table>
<hr>
<h2 id="quick-start">Quick Start</h2>
<h3>Step 1: Configure Your Admin Workstation</h3>
<p>Before connecting to remote PCs, run this <strong>once</strong> on your admin workstation (as Administrator):</p>
<pre><code>Set-Item WSMan:\localhost\Client\TrustedHosts -Value <span class="string">"*.logon.ds.ge.com"</span> -Force</code></pre>
<p>This allows your workstation to connect to any PC in the domain.</p>
<h3>Step 2: Configure the Setup Script</h3>
<p>Edit <code>Setup-WinRM.bat</code> and update these values at the top:</p>
<pre><code><span class="comment">REM Default security group - who can use WinRM to connect</span>
set "DEFAULT_SECURITY_GROUP=<span class="string">logon\groupid</span>"
<span class="comment">REM Where to log the inventory CSV (network share recommended)</span>
set "DEFAULT_LOG_PATH=<span class="string">\\server\share\winrm-inventory</span>"
<span class="comment">REM Domain suffix for TrustedHosts</span>
set "TRUSTED_DOMAIN=<span class="string">*.logon.ds.ge.com</span>"
<span class="comment">REM Optional: Trust a specific subnet (uncomment and set)</span>
<span class="comment">REM set "TRUSTED_SUBNET=10.48.130.*"</span></code></pre>
<h3>Step 3: Create Security Group in Active Directory</h3>
<ol>
<li>Open <strong>Active Directory Users and Computers</strong></li>
<li>Create a new Security Group (or use existing group matching <code>groupid</code>)</li>
<li>Add users who should have remote management access</li>
</ol>
<h3>Step 4: Run Setup on Each Shopfloor PC</h3>
<p>Run as Administrator on each PC:</p>
<pre><code>Setup-WinRM.bat</code></pre>
<p>Or with parameters:</p>
<pre><code>Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"</code></pre>
<p>The script will:</p>
<ul>
<li>Enable WinRM service</li>
<li>Configure authentication (Negotiate/Kerberos)</li>
<li>Set firewall rules (domain profile only)</li>
<li>Restrict access to the security group</li>
<li>Log hostname/IP to CSV inventory</li>
</ul>
<h3>Step 5: Run Remote Tasks</h3>
<p>From your admin workstation, edit <code>hosts.txt</code> with target PCs, then:</p>
<pre><code><span class="comment"># Test connectivity</span>
.\Invoke-RemoteTask.ps1 -Task TestConnection
<span class="comment"># Restart print spooler on all hosts</span>
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
<span class="comment"># Check disk space</span>
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace
<span class="comment"># Run on a single PC</span>
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span> -Task FlushDNS</code></pre>
<hr>
<h2 id="setup-details">Setup Script Details</h2>
<h3>What Setup-WinRM.bat Configures</h3>
<table>
<tr><th>Setting</th><th>Value</th><th>Purpose</th></tr>
<tr><td>WinRM Service</td><td>Auto-start</td><td>Ensures WinRM starts on boot</td></tr>
<tr><td>AllowUnencrypted</td><td>false</td><td>Security: require encrypted connections</td></tr>
<tr><td>Negotiate Auth</td><td>true</td><td>Enables Kerberos/NTLM authentication</td></tr>
<tr><td>CredSSP Auth</td><td>true</td><td>Enables credential delegation (double-hop)</td></tr>
<tr><td>Firewall</td><td>Domain profile</td><td>Opens port 5985 for domain connections only</td></tr>
<tr><td>TrustedHosts</td><td>*.logon.ds.ge.com</td><td>Trusts domain-joined PCs</td></tr>
<tr><td>RootSDDL</td><td>Security group</td><td>Restricts who can connect</td></tr>
</table>
<h3>CSV Inventory</h3>
<p>The setup script logs each PC to a CSV file:</p>
<pre><code>Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup
PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid
PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid</code></pre>
<p>You can use this CSV as your hosts file:</p>
<pre><code><span class="comment"># Extract hostnames from CSV</span>
Import-Csv <span class="string">"\\server\share\winrm-inventory\winrm-inventory.csv"</span> |
Select-Object -ExpandProperty Hostname |
Set-Content .\hosts.txt</code></pre>
<hr>
<h2 id="tasks">Available Remote Tasks</h2>
<table>
<tr><th>Task</th><th>Description</th></tr>
<tr><td><code>TestConnection</code></td><td>Verify WinRM connectivity</td></tr>
<tr><td><code>GetUptime</code></td><td>Show system uptime and last boot time</td></tr>
<tr><td><code>GetDiskSpace</code></td><td>Show free space on all drives</td></tr>
<tr><td><code>RestartSpooler</code></td><td>Restart Print Spooler service</td></tr>
<tr><td><code>FlushDNS</code></td><td>Clear DNS resolver cache</td></tr>
<tr><td><code>ClearTempFiles</code></td><td>Delete Windows temp files</td></tr>
<tr><td><code>DiskCleanup</code></td><td>Run Windows Disk Cleanup</td></tr>
<tr><td><code>OptimizeDisk</code></td><td>TRIM (SSD) or Defrag (HDD)</td></tr>
<tr><td><code>SyncTime</code></td><td>Force time sync with domain controller</td></tr>
<tr><td><code>RestartService</code></td><td>Restart any Windows service (requires <code>-ServiceName</code>)</td></tr>
<tr><td><code>RunCommand</code></td><td>Run custom PowerShell command (requires <code>-Command</code>)</td></tr>
<tr><td><code>RestartComputer</code></td><td>Restart the remote PC (requires YES confirmation)</td></tr>
</table>
<h3>Examples</h3>
<pre><code><span class="comment"># Check uptime on all hosts</span>
.\Invoke-RemoteTask.ps1 -Task GetUptime
<span class="comment"># Restart a specific service</span>
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName <span class="string">"Spooler"</span>
<span class="comment"># Run custom command</span>
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command <span class="string">"Get-Process | Sort CPU -Desc | Select -First 5"</span>
<span class="comment"># Use custom hosts file</span>
.\Invoke-RemoteTask.ps1 -HostsFile <span class="string">".\cnc-machines.txt"</span> -Task FlushDNS
<span class="comment"># Specify DNS suffix for short hostnames</span>
.\Invoke-RemoteTask.ps1 -DnsSuffix <span class="string">"logon.ds.ge.com"</span> -Task TestConnection
<span class="comment"># Restart a remote PC (will prompt for confirmation)</span>
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span> -Task RestartComputer
<span class="comment"># Increase parallelism for faster execution on many PCs</span>
.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20
<span class="comment"># Save results to a log file</span>
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults</code></pre>
<h3>Targeting Multiple PCs</h3>
<pre><code><span class="comment"># Comma-separated list</span>
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span>,<span class="string">"PC002"</span>,<span class="string">"PC003"</span> -Task GetUptime
<span class="comment"># Array variable</span>
$pcs = @(<span class="string">"PC001"</span>, <span class="string">"PC002"</span>, <span class="string">"PC003"</span>)
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS
<span class="comment"># From hosts.txt file (default)</span>
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
<span class="comment"># From CSV inventory</span>
$pcs = (Import-Csv <span class="string">"\\server\share\winrm-inventory.csv"</span>).Hostname
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace
<span class="comment"># From Active Directory query</span>
$pcs = (Get-ADComputer -Filter <span class="string">"Name -like 'SHOPFLOOR-*'"</span>).Name
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime</code></pre>
<div class="info">
<strong>Parallel Execution:</strong> All commands run in parallel (default: 10 concurrent connections). Adjust with <code>-ThrottleLimit</code> parameter.
</div>
<h3>Logging Results</h3>
<p>Use <code>-LogResults</code> to save task output to a timestamped log file in the script directory:</p>
<pre><code>.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults
<span class="comment"># Creates: RemoteTask_20260108_143022.log</span></code></pre>
<p>Log files contain:</p>
<ul>
<li>Task name and parameters</li>
<li>Execution time</li>
<li>Status of each computer (OK/FAIL)</li>
<li>Result messages</li>
<li>Summary totals</li>
</ul>
<hr>
<h2 id="troubleshooting">Troubleshooting</h2>
<h3>"Access Denied" when connecting</h3>
<ol>
<li>Verify you're a member of the WinRM security group</li>
<li>Check that your credentials are correct</li>
<li>Verify the target PC ran Setup-WinRM.bat successfully</li>
</ol>
<h3>"WinRM cannot complete the operation"</h3>
<ol>
<li>Verify the target PC is reachable: <code>ping PC001</code></li>
<li>Check WinRM is running on target: <code>sc query winrm</code> (on target PC)</li>
<li>Verify firewall allows port 5985</li>
</ol>
<h3>"The WinRM client cannot process the request"</h3>
<p>Add target to TrustedHosts on your admin workstation:</p>
<pre><code>Set-Item WSMan:\localhost\Client\TrustedHosts -Value <span class="string">"*.logon.ds.ge.com"</span> -Force</code></pre>
<h3>Test WinRM Configuration</h3>
<p>On your admin workstation:</p>
<pre><code><span class="comment"># Test basic connectivity</span>
Test-WSMan -ComputerName PC001
<span class="comment"># Test with credentials</span>
$cred = Get-Credential
Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate
<span class="comment"># Enter interactive session</span>
Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate</code></pre>
<p>On the target PC:</p>
<pre><code>winrm enumerate winrm/config/listener
winrm get winrm/config/service</code></pre>
<hr>
<h2 id="security">Security Considerations</h2>
<div class="warning">
<strong>Important Security Notes:</strong>
</div>
<ol>
<li><strong>Use Security Groups</strong>: Always restrict WinRM access to a specific AD group</li>
<li><strong>Domain Profile Only</strong>: Firewall rules only allow connections on domain networks</li>
<li><strong>No Unencrypted Traffic</strong>: AllowUnencrypted is set to false</li>
<li><strong>Audit Access</strong>: Enable Windows Security auditing for logon events</li>
<li><strong>Credential Protection</strong>: Use dedicated admin accounts, not personal accounts</li>
</ol>
<hr>
<h2 id="custom-tasks">Adding Custom Tasks</h2>
<p>Edit <code>Invoke-RemoteTask.ps1</code> and add to the <code>$TaskScripts</code> hashtable:</p>
<pre><code><span class="string">'MyCustomTask'</span> = {
$result = @{
Success = <span class="keyword">$false</span>
Hostname = $env:COMPUTERNAME
Output = <span class="string">""</span>
Error = <span class="keyword">$null</span>
}
<span class="keyword">try</span> {
<span class="comment"># Your code here</span>
$result.Output = <span class="string">"Task completed"</span>
$result.Success = <span class="keyword">$true</span>
} <span class="keyword">catch</span> {
$result.Error = $_.Exception.Message
}
<span class="keyword">return</span> $result
}</code></pre>
<p>Then add the task name to the <code>ValidateSet</code> in the param block.</p>
<hr>
<h2>Support</h2>
<p>For issues or questions, contact your IT support team.</p>
<p style="text-align: center; color: #666; margin-top: 40px; font-size: 0.9em;">
WinRM Setup Package &copy; 2026 | Generated from README.md
</p>
</body>
</html>

View File

@@ -0,0 +1,16 @@
# WinRM Target Hosts
# -------------------
# Add one hostname or IP address per line
# Lines starting with # are comments
#
# You can use:
# - Hostnames: PC001, SHOPFLOOR-PC-01
# - FQDNs: PC001.logon.ds.ge.com
# - IP addresses: 10.48.130.100
#
# Example:
# PC001
# PC002
# 10.48.130.100
# shopfloor-cnc-01.logon.ds.ge.com