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

7
.gitignore vendored
View File

@@ -15,6 +15,13 @@ logs/
# CSV data files (generated)
applications.csv
# Inventory data (contains CUI / employee SSO / MAC addresses)
inventory.csv
inventory.xlsx
# Per-host log files written by remote-execution scripts
logs_*.txt
# Text files with hostnames/IPs (sensitive)
computers.txt
shopfloor-pcs.txt

View File

@@ -614,11 +614,19 @@ function Get-PCType {
if ($domain -eq "logon.ds.ge.com") {
Write-Host " [OK] Shopfloor domain detected" -ForegroundColor Green
# Check for specific machine type applications
$installedApps = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName } |
Select-Object -ExpandProperty DisplayName
# Check for specific machine type applications (include per-user installs)
$regPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$installedApps = foreach ($regPath in $regPaths) {
if (Test-Path $regPath) {
Get-ItemProperty $regPath -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName } |
Select-Object -ExpandProperty DisplayName
}
}
# ================================================================
# PC Type Detection based on installed software
@@ -821,6 +829,9 @@ function Collect-SystemInfo {
$installedApps = @()
$installedApps += Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
$installedApps += Get-ItemProperty HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
if (Test-Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*") {
$installedApps += Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName}
}
$filteredApps = $installedApps | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName -Unique

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
## Table of Contents
- [Overview](#overview)
- [First-Time Setup](#first-time-setup)
- [API Integration](#api-integration)
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
@@ -17,6 +18,8 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
- [Service Management](#how-to-manage-services)
- [Time Synchronization](#how-to-fix-time-sync-issues)
- [DNC Configuration](#how-to-update-dnc-configurations)
- [File Deployment](#how-to-deploy-files)
- [Registry Import](#how-to-import-registry-files)
- [Software Deployment](#how-to-deploy-software)
- [Batch Operations](#how-to-run-batch-operations)
- [Targeting Strategies](#targeting-strategies)
@@ -32,13 +35,53 @@ This script provides a comprehensive remote maintenance toolkit for managing sho
**Location:** `S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1`
**Key Features:**
- 19 maintenance tasks available
- 22 maintenance tasks available
- General-purpose file deployment (`CopyFile`) and registry import (`ImportReg`) tasks
- Registry imports run as logged-in user via scheduled task (HKCU support)
- Optional post-copy commands via scheduled task (service/app restarts)
- Multiple targeting options (by name, type, business unit, or all)
- Concurrent execution with configurable throttling
- Integration with ShopDB for PC discovery
---
## First-Time Setup
Before running this script for the first time, you must allow PowerShell script execution and unblock the script file.
### Step 1: Allow PowerShell Script Execution
Open PowerShell **as Administrator** and run:
```powershell
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
```
When prompted, type `Y` and press Enter. This allows locally-created scripts to run and requires downloaded scripts to be signed or unblocked.
> **Note:** You only need to do this once per user account. `RemoteSigned` is the recommended policy — it allows local scripts while still protecting against untrusted downloads.
### Step 2: Unblock the Script File
Files downloaded from a network share or the internet are marked as "blocked" by Windows. You must unblock the script before it can run.
**Option A — File Explorer (GUI):**
1. Navigate to `S:\dt\shopfloor\scripts\remote-execution\`
2. Right-click `Invoke-RemoteMaintenance.ps1` and select **Properties**
3. At the bottom of the General tab, check the **Unblock** checkbox
4. Click **Apply**, then **OK**
**Option B — PowerShell:**
```powershell
Unblock-File -Path "S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1"
```
> **Important:** If you skip this step, you will get a security error when trying to run the script: *"cannot be loaded because running scripts is disabled on this system"* or *"cannot be loaded. The file is not digitally signed."*
---
## API Integration
When using `-All`, `-PcType`, or `-BusinessUnit` targeting, the script retrieves PC lists from the ShopDB API:
@@ -53,12 +96,12 @@ GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit
| ID | Type | ID | Type |
|----|------|----|------|
| 1 | Shopfloor | 7 | Heat Treat |
| 2 | CMM | 8 | Engineer |
| 3 | Wax Trace | 9 | Standard |
| 4 | Keyence | 10 | Inspection |
| 5 | EAS1000 | 11 | Dashboard |
| 6 | Genspect | 12 | Lobby Display |
| 1 | Standard | 7 | Keyence |
| 2 | Engineer | 8 | Genspect |
| 3 | Shopfloor | 9 | Heat Treat |
| 4 | Uncategorized | 10 | Inspection |
| 5 | CMM | 11 | Dashboard |
| 6 | Wax / Trace | 12 | Lobby Display |
**See:** [ShopDB API Reference](ShopDB-API.html) for complete API documentation.
@@ -133,6 +176,15 @@ The script outputs status for each PC:
|-----------|------|-------------|
| `-Task` | string | Maintenance task to execute |
### File Deployment Parameters (CopyFile / ImportReg)
| Parameter | Type | Description |
|-----------|------|-------------|
| `-SourcePath` | string | Source file path (local or UNC). Required for CopyFile and ImportReg |
| `-DestinationPath` | string | Destination file path on remote PCs. Required for CopyFile |
| `-RunCommand` | string | Command to run after CopyFile. Runs as logged-in user by default (via scheduled task) |
| `-AsSystem` | switch | Run ImportReg and -RunCommand in the WinRM session (SYSTEM context) instead of as logged-in user |
### Optional Parameters
| Parameter | Type | Default | Description |
@@ -140,7 +192,8 @@ The script outputs status for each PC:
| `-Credential` | PSCredential | Prompt | Remote authentication |
| `-ApiUrl` | string | Production | ShopDB API endpoint |
| `-ThrottleLimit` | int | 5 | Max concurrent sessions |
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for resolution |
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for FQDN resolution |
| `-LogFile` | switch | Off | Enable transcript logging to `logs/` directory |
### PC Types
@@ -194,13 +247,22 @@ TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `UpdateEMxAuthToken` | Update eMx auth from share | 1-2 min | None |
| `DeployUDCWebServerConfig` | Deploy UDC config | 1-2 min | None |
| `UpdateDNCMXHosts` | Update FtpHostPrimary/Secondary in DNC\MX registry | <1 min | None |
| `AuditDNCConfig` | Compare DNC registry vs UDC backup JSON, export CSV | 1-5 min | None |
| `CheckDefectTracker` | Check if Defect_Tracker.exe is running, export CSV | 1-5 min | None |
### File Deployment Tasks
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `CopyFile` | Copy file from `-SourcePath` to `-DestinationPath` on remote PCs | 1-2 min | Low |
| `ImportReg` | Copy `.reg` file and import via scheduled task as logged-in user | 1-2 min | Low |
### System Tasks
| Task | Description | Duration | Impact |
|------|-------------|----------|--------|
| `GPUpdate` | Force Group Policy refresh (`gpupdate /force`) | <1 min | Low |
| `Reboot` | Restart PC (30s delay) | 2-5 min | High |
### Software Deployment Tasks
@@ -222,18 +284,19 @@ Deployment tasks require source files to be available before execution:
| Task | Source File Path |
|------|------------------|
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
| `UpdateEMxAuthToken` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt` |
| `DeployUDCWebServerConfig` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json` |
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
| `CopyFile` | Any file - specified via `-SourcePath` parameter |
| `ImportReg` | Any `.reg` file - specified via `-SourcePath` parameter |
### How Deployment Works
1. **Pre-flight Check:** Script verifies source file exists
2. **WinRM Session:** Opens remote session to target PC
3. **File Push:** Copies source file to `C:\Windows\Temp\` on remote PC
4. **Execution:** Runs install/copy task using pushed file
5. **Cleanup:** Removes temp file from remote PC
4. **Execution:** Runs install/copy/import task using pushed file
5. **Post-action:** Optionally runs command as logged-in user via scheduled task
6. **Cleanup:** Removes temp file from remote PC
```
+---------------------+ WinRM +---------------------+
@@ -242,50 +305,44 @@ Deployment tasks require source files to be available before execution:
| Source Files: | Push File | Temp Location: |
| - Setup.exe | ------------> | C:\Windows\Temp |
| - config.json | | |
| - eMxInfo.txt | Execute | Final Location: |
| (network) | ------------> | C:\Program Files |
| - settings.reg | Execute | Final Location: |
| (any path/UNC) | ------------> | -DestinationPath |
| | | |
| | Sched. Task | Logged-in User: |
| | ------------> | - regedit /s |
| | | - RunCommand |
+---------------------+ +---------------------+
```
### Directory Structure
### CopyFile Details
Ensure your script directory contains the required files:
The `CopyFile` task:
```
S:\dt\shopfloor\scripts\remote-execution\
├── Invoke-RemoteMaintenance.ps1
├── GEAerospaceDashboardSetup.exe # For InstallDashboard
├── GEAerospaceLobbyDisplaySetup.exe # For InstallLobbyDisplay
└── udc_webserver_settings.json # For DeployUDCWebServerConfig
```
1. **Source:** Any file specified via `-SourcePath` (local or UNC path)
2. **Destination:** Specified via `-DestinationPath`
3. **Backup:** Existing file is backed up as `<name>-old-<timestamp>.<ext>`
4. **Post-action:** If `-RunCommand` is specified, runs as the logged-in user via a one-shot scheduled task (same pattern as Dashboard/Lobby kiosk relaunch)
### eMx Auth Token Details
### ImportReg Details
The `UpdateEMxAuthToken` task:
The `ImportReg` task:
1. **Source:** `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt`
2. **Destinations:** (both paths if they exist)
- `C:\Program Files\GE Aircraft Engines\DNC\eMxInfo.txt`
- `C:\Program Files (x86)\GE Aircraft Engines\DNC\eMxInfo.txt`
3. **Backup:** Creates `eMxInfo-old-YYYYMMDD-HHMMSS.txt` before overwriting
4. **Post-action:** Restarts DNC service (`LDnc.exe`)
### UDC Web Server Config Details
The `DeployUDCWebServerConfig` task:
1. **Pre-check:** Verifies UDC is installed (`C:\Program Files\UDC` exists)
2. **Skip:** PCs without UDC are skipped (not counted as failures)
3. **Destination:** `C:\ProgramData\UDC\udc_webserver_settings.json`
4. **Backup:** Creates backup before overwriting
1. **Source:** `.reg` file specified via `-SourcePath`
2. **Import method:** `regedit.exe /s` via one-shot scheduled task as logged-in user
3. **HKCU support:** Runs as the logged-in user, so both HKLM and HKCU keys apply correctly
4. **Fallback:** If no user is logged in, runs `regedit.exe /s` directly (HKLM only)
5. **Cleanup:** Temp `.reg` file removed after import
### Dashboard/Lobby Display Install Details
Both kiosk app installers:
1. **Installer type:** Inno Setup (supports `/VERYSILENT`)
2. **Execution:** Silent install with no user prompts
3. **Cleanup:** Installer removed from temp after execution
2. **Pre-install:** Kills running Edge kiosk via `PrepareToInstall` in Inno Setup
3. **Execution:** Silent install with no user prompts (120-second timeout)
4. **Post-install:** Creates a one-shot scheduled task to relaunch Edge in kiosk mode as the logged-in user (e.g. `lg044513sd`), then auto-deletes the task
5. **Cleanup:** Installer removed from temp after execution
6. **Connectivity:** Offline PCs are skipped with a ping check before connecting
**Uninstall GUIDs:**
- Dashboard: `{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}`
@@ -295,7 +352,7 @@ Both kiosk app installers:
To add a new application for deployment, edit the script in two places:
**Step 1: Add to `$KioskAppConfig` hashtable (~line 1388)**
**Step 1: Add to `$KioskAppConfig` hashtable**
```powershell
$KioskAppConfig = @{
@@ -304,10 +361,11 @@ $KioskAppConfig = @{
# Add new application
'InstallNewApp' = @{
Action = 'Install'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
InstallerName = 'NewAppSetup.exe'
AppName = 'New Application Name'
UninstallGuid = '{YOUR-GUID-HERE}' # Find in registry after manual install
KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/your-page/' # Optional: relaunch Edge kiosk after install
}
'UninstallNewApp' = @{
Action = 'Uninstall'
@@ -334,7 +392,7 @@ $KioskAppConfig = @{
**Step 3: Place installer on network share**
```
\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
```
**Finding the Uninstall GUID:**
@@ -562,37 +620,129 @@ wuauclt /detectnow
### How to Update DNC Configurations
#### Update eMx Authentication Token
#### Update DNC MX Hosts
**Scenario:** eMx authentication is failing on DNC PCs.
**Scenario:** FtpHostPrimary/FtpHostSecondary in DNC\MX registry needs updating (hostname migration).
```powershell
# Single PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateEMxAuthToken -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateDNCMXHosts -Credential $cred
# All shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateEMxAuthToken -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateDNCMXHosts -Credential $cred
```
**What it does:**
1. Backs up existing `eMxInfo.txt` with timestamp
2. Copies new token file from network share
3. Verifies file was updated
1. Checks both 32-bit (WOW6432Node) and 64-bit registry paths
2. Only updates values matching the old hostname - skips unexpected values
3. Safe to run on all PCs: no-ops if DNC\MX key doesn't exist
#### Deploy UDC Web Server Configuration
#### Audit DNC Config vs UDC Backup
**Scenario:** UDC web server settings need to be updated.
**Scenario:** Verify DNC registry settings match UDC backup JSON files.
```powershell
# Deploy to PCs with UDC installed
.\Invoke-RemoteMaintenance.ps1 -ComputerName "UDC-PC01","UDC-PC02" -Task DeployUDCWebServerConfig -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -All -Task AuditDNCConfig -Credential $cred -LogFile
```
**What it does:**
1. Checks if UDC is installed
2. Backs up existing configuration
3. Deploys new web server settings
4. Does NOT restart UDC (requires manual restart)
1. Reads DNC registry values (General, eFocas, Hssb, PPDCS keys)
2. Compares against UDC backup JSON files on the network share
3. Reports MATCH/MISMATCH/MISSING for each field
4. Exports CSV report to `logs/`
#### Check Defect Tracker Status
```powershell
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CheckDefectTracker -Credential $cred
```
**What it does:**
1. Checks if `Defect_Tracker.exe` is running on each PC
2. Reports machine number + running status
3. Exports CSV report to `logs/`
---
### How to Deploy Files
**Scenario:** Push any file to remote PCs with automatic backup of existing files.
#### Basic File Copy
```powershell
# Copy a config file to a specific destination
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task CopyFile `
-SourcePath "\\server\share\config.json" `
-DestinationPath "C:\ProgramData\App\config.json" `
-Credential $cred
```
**What it does:**
1. Copies source file to `C:\Windows\Temp\` on remote PC via WinRM
2. Creates backup of existing file (if any) with timestamp
3. Moves temp file to final destination
4. Verifies deployment
#### File Copy with Post-Copy Command
**Scenario:** Deploy a file and restart a service/app in the logged-in user's session.
```powershell
# Deploy eMxInfo.txt and kill DNC so it picks up the new file
.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile `
-SourcePath "\\server\share\eMxInfo.txt" `
-DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" `
-RunCommand "taskkill /IM DNCMain.exe /F" `
-Credential $cred
# Deploy UDC config (no restart needed)
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CopyFile `
-SourcePath "\\server\share\udc_webserver_settings.json" `
-DestinationPath "C:\ProgramData\UDC\udc_webserver_settings.json" `
-Credential $cred
```
The `-RunCommand` runs via a one-shot scheduled task as the logged-in user, so it works for user-session processes (same pattern as Dashboard/Lobby Display kiosk relaunch).
---
### How to Import Registry Files
**Scenario:** Apply registry settings from a `.reg` file to remote PCs.
```powershell
# Import a .reg file on all shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg `
-SourcePath "\\server\share\intranet-zone.reg" `
-Credential $cred
# Import on specific PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ImportReg `
-SourcePath "C:\Scripts\my-settings.reg" `
-Credential $cred
```
**What it does (default — as logged-in user):**
1. Copies `.reg` file to `C:\Windows\Temp\` on remote PC via WinRM
2. Creates a one-shot scheduled task as the logged-in user
3. Runs `regedit.exe /s` to silently import the registry file
4. Cleans up temp file and scheduled task
**HKCU support:** Because the import runs as the logged-in user (via scheduled task), both `HKLM` and `HKCU` keys in the `.reg` file are applied correctly. If no user is logged in, it falls back to direct import (HKLM only).
#### HKLM-Only / System Context
Use `-AsSystem` when the `.reg` file only contains `HKLM` keys and you want to skip the scheduled task overhead:
```powershell
# Import HKLM-only registry settings directly as SYSTEM
.\Invoke-RemoteMaintenance.ps1 -All -Task ImportReg `
-SourcePath "\\server\share\machine-policy.reg" `
-AsSystem -Credential $cred
```
This runs `regedit.exe /s` directly in the WinRM session (SYSTEM context). Faster, but HKCU keys will not apply to any user.
---
@@ -612,22 +762,21 @@ $kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03")
```
**What it does:**
1. Copies installer from network share
2. Runs silent installation
3. Configures auto-start
4. Cleans up installer
1. Pings target PC (skips if offline)
2. Copies installer from network share to `C:\Windows\Temp\`
3. Kills running Edge kiosk
4. Runs silent installation (120-second timeout)
5. Relaunches Edge kiosk via scheduled task as the logged-in user
6. Cleans up installer and scheduled task
**After installation:**
- Run data collection to update PC type
- Reboot PC to complete setup
**No reboot required** — Edge relaunches automatically in the logged-in user's session.
```powershell
# Complete Dashboard deployment sequence
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task InstallDashboard -Credential $cred
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task Reboot -Credential $cred
# Deploy to all Dashboard kiosks
.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task InstallDashboard -Credential $cred
# After reboot, update ShopDB
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "KIOSK-01" -Credential $cred
# Deploy to all Lobby Display kiosks
.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task InstallLobbyDisplay -Credential $cred
```
#### Install Lobby Display

View File

@@ -23,7 +23,7 @@ powershell-scripts/
## Table of Contents
1. [Asset Collection Scripts](#asset-collection-scripts) (`asset-collection/`)
2. [Remote Execution Scripts](#remote-execution-scripts) (`remote-execution/`)
2. [Remote Execution Scripts](#remote-execution-scripts) (`S:\dt\shopfloor\scripts\remote-execution\`)
3. [Setup & Utility Scripts](#setup--utility-scripts) (`setup-utilities/`)
4. [Registry Backup Scripts](#registry-backup-scripts) (`registry-backup/`)
5. [WinRM HTTPS Scripts](#winrm-https-scripts) (`winrm-https/`)

308
docs/convert_to_docx.py Normal file
View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
Convert Markdown documentation to Word documents (.docx)
With proper code block formatting (shaded boxes)
"""
import re
import os
from docx import Document
from docx.shared import Inches, Pt, RGBColor, Twips
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn, nsmap
from docx.oxml import OxmlElement
def set_cell_shading(cell, color="E8E8E8"):
"""Set cell background shading color."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:fill'), color)
shd.set(qn('w:val'), 'clear')
tcPr.append(shd)
def set_cell_borders(cell, color="CCCCCC"):
"""Set cell border color."""
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcBorders = OxmlElement('w:tcBorders')
for border_name in ['top', 'left', 'bottom', 'right']:
border = OxmlElement(f'w:{border_name}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '4')
border.set(qn('w:color'), color)
tcBorders.append(border)
tcPr.append(tcBorders)
def add_code_block(doc, code_text, language=""):
"""Add a formatted code block with shading."""
# Create a single-cell table for the code block
table = doc.add_table(rows=1, cols=1)
table.autofit = True
cell = table.rows[0].cells[0]
# Set cell shading (light gray background)
set_cell_shading(cell, "F5F5F5")
set_cell_borders(cell, "DDDDDD")
# Clear default paragraph and add code
cell.paragraphs[0].clear()
# Add each line of code
lines = code_text.split('\n')
for i, line in enumerate(lines):
if i == 0:
para = cell.paragraphs[0]
else:
para = cell.add_paragraph()
para.paragraph_format.space_before = Pt(0)
para.paragraph_format.space_after = Pt(0)
para.paragraph_format.line_spacing = 1.0
run = para.add_run(line if line else ' ') # Use space for empty lines
run.font.name = 'Consolas'
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0, 0, 0)
# Add spacing after the code block
doc.add_paragraph()
def parse_markdown(md_content):
"""Parse markdown content into structured elements."""
lines = md_content.split('\n')
elements = []
i = 0
while i < len(lines):
line = lines[i]
# Skip empty lines
if not line.strip():
i += 1
continue
# Headers
if line.startswith('# '):
elements.append(('h1', line[2:].strip()))
i += 1
elif line.startswith('## '):
elements.append(('h2', line[3:].strip()))
i += 1
elif line.startswith('### '):
elements.append(('h3', line[4:].strip()))
i += 1
elif line.startswith('#### '):
elements.append(('h4', line[5:].strip()))
i += 1
# Horizontal rule
elif line.strip() == '---':
elements.append(('hr', ''))
i += 1
# Code blocks
elif line.strip().startswith('```'):
code_lang = line.strip()[3:]
code_lines = []
i += 1
while i < len(lines) and not lines[i].strip().startswith('```'):
code_lines.append(lines[i])
i += 1
# Store language info with code
elements.append(('code', (code_lang, '\n'.join(code_lines))))
i += 1 # Skip closing ```
# Tables
elif '|' in line and i + 1 < len(lines) and '---' in lines[i + 1]:
table_lines = [line]
i += 1
while i < len(lines) and '|' in lines[i]:
table_lines.append(lines[i])
i += 1
elements.append(('table', table_lines))
# Bullet lists
elif line.strip().startswith('- ') or line.strip().startswith('* '):
list_items = []
while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ') or (lines[i].startswith(' ') and lines[i].strip())):
if lines[i].strip().startswith('- ') or lines[i].strip().startswith('* '):
list_items.append(lines[i].strip()[2:])
elif lines[i].startswith(' ') and list_items:
list_items[-1] += ' ' + lines[i].strip()
i += 1
elements.append(('bullet', list_items))
# Numbered lists
elif re.match(r'^\d+\.\s', line.strip()):
list_items = []
while i < len(lines) and (re.match(r'^\d+\.\s', lines[i].strip()) or lines[i].startswith(' ')):
if re.match(r'^\d+\.\s', lines[i].strip()):
list_items.append(re.sub(r'^\d+\.\s', '', lines[i].strip()))
elif lines[i].startswith(' ') and list_items:
list_items[-1] += ' ' + lines[i].strip()
i += 1
elements.append(('numbered', list_items))
# Regular paragraph
else:
para_lines = [line]
i += 1
while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('```') and not lines[i].startswith('- ') and not lines[i].startswith('* ') and '|' not in lines[i] and not re.match(r'^\d+\.\s', lines[i].strip()):
para_lines.append(lines[i])
i += 1
elements.append(('para', ' '.join(para_lines)))
return elements
def clean_text(text):
"""Remove markdown formatting from text."""
# Bold
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
# Italic
text = re.sub(r'\*([^*]+)\*', r'\1', text)
# Code
text = re.sub(r'`([^`]+)`', r'\1', text)
# Links
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
return text
def add_formatted_text(paragraph, text):
"""Add text with basic formatting to a paragraph."""
# Split by formatting markers and add runs
parts = re.split(r'(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))', text)
for part in parts:
if not part:
continue
if part.startswith('**') and part.endswith('**'):
run = paragraph.add_run(part[2:-2])
run.bold = True
elif part.startswith('`') and part.endswith('`'):
run = paragraph.add_run(part[1:-1])
run.font.name = 'Consolas'
run.font.size = Pt(9)
# Add light background for inline code
run.font.highlight_color = 15 # Light gray (WD_COLOR_INDEX.GRAY_25)
elif part.startswith('[') and '](' in part:
match = re.match(r'\[([^\]]+)\]\(([^)]+)\)', part)
if match:
run = paragraph.add_run(match.group(1))
run.font.color.rgb = RGBColor(0, 0, 255)
run.underline = True
else:
paragraph.add_run(part)
def convert_md_to_docx(md_file, docx_file):
"""Convert a markdown file to a Word document."""
print(f"Converting {md_file} to {docx_file}...")
with open(md_file, 'r', encoding='utf-8') as f:
content = f.read()
elements = parse_markdown(content)
doc = Document()
# Set default font
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(11)
for elem_type, elem_content in elements:
if elem_type == 'h1':
p = doc.add_heading(clean_text(elem_content), level=0)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif elem_type == 'h2':
doc.add_heading(clean_text(elem_content), level=1)
elif elem_type == 'h3':
doc.add_heading(clean_text(elem_content), level=2)
elif elem_type == 'h4':
doc.add_heading(clean_text(elem_content), level=3)
elif elem_type == 'hr':
p = doc.add_paragraph()
p.add_run('' * 70)
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif elem_type == 'para':
p = doc.add_paragraph()
add_formatted_text(p, elem_content)
elif elem_type == 'code':
code_lang, code_text = elem_content
add_code_block(doc, code_text, code_lang)
elif elem_type == 'bullet':
for item in elem_content:
p = doc.add_paragraph(style='List Bullet')
add_formatted_text(p, item)
elif elem_type == 'numbered':
for item in elem_content:
p = doc.add_paragraph(style='List Number')
add_formatted_text(p, item)
elif elem_type == 'table':
# Parse table
rows = []
for line in elem_content:
if '---' in line:
continue
cells = [c.strip() for c in line.split('|')[1:-1]]
if cells:
rows.append(cells)
if rows:
num_cols = len(rows[0])
table = doc.add_table(rows=len(rows), cols=num_cols)
table.style = 'Table Grid'
table.alignment = WD_TABLE_ALIGNMENT.CENTER
for i, row in enumerate(rows):
for j, cell in enumerate(row):
if j < num_cols:
table.rows[i].cells[j].text = clean_text(cell)
# Bold and shade header row
if i == 0:
set_cell_shading(table.rows[i].cells[j], "E0E0E0")
for para in table.rows[i].cells[j].paragraphs:
for run in para.runs:
run.bold = True
# Add spacing after table
doc.add_paragraph()
doc.save(docx_file)
print(f" Created: {docx_file}")
def main():
docs_dir = '/home/camp/projects/powershell/docs'
md_files = [
'Update-ShopfloorPCs-Remote.md',
'Invoke-RemoteMaintenance.md',
'Update-PC-CompleteAsset.md',
'DATA_COLLECTION_PARITY.md'
]
for md_file in md_files:
md_path = os.path.join(docs_dir, md_file)
docx_path = os.path.join(docs_dir, md_file.replace('.md', '.docx'))
if os.path.exists(md_path):
convert_md_to_docx(md_path, docx_path)
else:
print(f"Warning: {md_path} not found")
print("\nConversion complete!")
print(f"Word documents saved to: {docs_dir}")
if __name__ == '__main__':
main()

Submodule edncfix updated: 28641c47c5...2748bfa037

File diff suppressed because it is too large Load Diff

View File

@@ -160,9 +160,9 @@ try {
# Machine numbers that indicate specific PC types (these ARE valid machine numbers)
$machineTypeIndicators = @{
"^0?600$" = "Wax Trace" # Wax trace machines
"^0?(612|613|615)$" = "Part Marker" # Part marker machines
"^M?(612|613|615)$" = "Part Marker" # Part marker machines (M prefix)
"^8003$" = "Part Marker" # Part marker machines
"^0?(612|613|615)$" = "Inspection" # Part marker machines
"^M?(612|613|615)$" = "Inspection" # Part marker machines (M prefix)
"^8003$" = "Inspection" # Part marker machines
}
# Check if machine number indicates a specific PC type

View File

@@ -0,0 +1,29 @@
# DeployOpenTextProfiles - Example Usage
# Source: \\tsgwp00525.wjs.geaerospace.net\shared\dt\csf\
# Single PC
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DeployOpenTextProfiles
# Multiple PCs
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF","G1ZTNCX4ESF","G1ZTNCX5ESF" -Task DeployOpenTextProfiles
# All shopfloor PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task DeployOpenTextProfiles
# Wax / Trace PCs
.\Invoke-RemoteMaintenance.ps1 -PcType "Wax / Trace" -Task DeployOpenTextProfiles
# Keyence PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Keyence -Task DeployOpenTextProfiles
# Genspect PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Genspect -Task DeployOpenTextProfiles
# Heat Treat PCs
.\Invoke-RemoteMaintenance.ps1 -PcType "Heat Treat" -Task DeployOpenTextProfiles
# CMM PCs
.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task DeployOpenTextProfiles
# Inspection PCs
.\Invoke-RemoteMaintenance.ps1 -PcType Inspection -Task DeployOpenTextProfiles

View File

@@ -13,8 +13,11 @@ REQUIREMENTS
- PowerShell 5.1+
- Run as Administrator (required for scheduling only)
- Invoke-RemoteMaintenance.ps1 in the same folder
- A PC list text file (one hostname per line)
- ALL scripts must be in the SAME folder:
Invoke-RemoteMaintenance.ps1
Schedule-Maintenance.ps1
Export-PCList.ps1
shopfloor-pcs.txt (or your PC list file)
============================================================
@@ -34,53 +37,92 @@ PARAMETERS
============================================================
USAGE
STEP 1: Save Credentials (one time)
============================================================
1. SAVE CREDENTIALS (one time, does not require admin)
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
- Encrypted with Windows DPAPI
- Only your user account on this machine can decrypt
- Re-run if your password changes
2. RUN IMMEDIATELY (does not require admin)
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
3. SCHEDULE A ONE-TIME TASK (requires admin)
# Reboot one PC today at 3:00 PM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\test-reboot.txt" -Task Reboot -TaskFrequency Once -TaskTime "15:00" -TaskDate "2026-02-19"
# Reboot all PCs Sunday Feb 22 at 12:01 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
4. SCHEDULE A RECURRING TASK (requires admin)
# Every Sunday at 12:01 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
# Every day at 2:00 AM
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task DiskCleanup -TaskFrequency Daily -TaskTime "02:00"
5. MANAGE SCHEDULED TASKS
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
- Encrypted with AES-256 key
- Works from normal or admin PowerShell
- Stored in .creds\ folder (not plaintext)
- Re-run if your password changes
============================================================
LOGS
STEP 2: Generate PC List
============================================================
.\logs\maintenance-YYYY-MM-DD_HHMMSS-TaskName.log
# All shopfloor PCs from API
.\Export-PCList.ps1
# Filter by type
.\Export-PCList.ps1 -PcType Shopfloor
# Single PC for testing
"G63TVG04ESF" | Out-File -FilePath ".\test-reboot.txt" -Encoding UTF8
============================================================
STEP 3: Run or Schedule
============================================================
RUN IMMEDIATELY (no admin needed):
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
SCHEDULE ONE-TIME (admin required):
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
SCHEDULE RECURRING (admin required):
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
============================================================
CHECKING RESULTS
============================================================
AFTER A SCHEDULED RUN:
Get-Content ".\logs\LAST-RUN-SUMMARY.txt"
FULL LOG (most recent):
Get-ChildItem ".\logs\" -Filter "maintenance-*-Reboot.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
CHECK IF TASK RAN:
Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo
LastTaskResult = 0 means success
Anything else means it errored before writing logs
LOGS LOCATION:
.\logs\ (inside your scripts folder)
============================================================
MANAGING SCHEDULED TASKS
============================================================
# List maintenance tasks
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
# Run now (don't wait for schedule)
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
# Delete a task
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
# Or use: Task Scheduler GUI (taskschd.msc)
============================================================
AVAILABLE TASKS
============================================================
Reboot, DISM, SFC, OptimizeDisk, DiskCleanup,
ClearUpdateCache, ClearBrowserCache, RestartSpooler,
FlushDNS, RestartWinRM, SetTimezone, SyncTime,
UpdateEMxAuthToken, DeployUDCWebServerConfig,
UpdateDNCMXHosts,
InstallDashboard, InstallLobbyDisplay,
UninstallDashboard, UninstallLobbyDisplay
============================================================
@@ -88,13 +130,24 @@ TROUBLESHOOTING
============================================================
"No saved credentials found"
-> Run -SaveCredential with -Username and -Password
-> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "pass"
"Access is denied" when scheduling
-> Right-click PowerShell -> Run as Administrator
"No credentials provided. Exiting."
-> GUI prompt failed. Use -Username and -Password flags
-> Use -Username and -Password flags instead of GUI prompt
No logs folder / empty logs
-> Task may not have run yet. Check:
Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo
Password changed
-> Re-run -SaveCredential with new password
-> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "newpass"
NOTE ABOUT "Running as AEROAD\SSO":
This is normal. The scheduled task runs as your Windows login.
It still uses your SAVED credentials for WinRM connections
to the remote shopfloor PCs. Two separate accounts:
1. Your Windows login = runs the script
2. Saved credentials = connects to remote PCs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
@echo off
:: Resume-Download — Resumable download for SharePoint and general URLs
:: Uses domain credentials for SharePoint auth
if "%~1"=="" (
set /p "URL=Enter download URL: "
) else (
set "URL=%~1"
)
if "%~2"=="" (
set /p "DEST=Save as (full path, e.g. C:\Temp\file.iso): "
) else (
set "DEST=%~2"
)
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Resume-Download.ps1" -Url "%URL%" -Destination "%DEST%"
pause

View File

@@ -0,0 +1,117 @@
param(
[Parameter(Mandatory=$true)]
[string]$Url,
[Parameter(Mandatory=$true)]
[string]$Destination
)
$ErrorActionPreference = 'Stop'
Write-Host "============================================"
Write-Host " Resumable File Download"
Write-Host "============================================"
Write-Host ""
Write-Host "Destination: $Destination"
Write-Host ""
try {
# Check for partial file to resume
$startBytes = 0
if (Test-Path $Destination) {
$startBytes = (Get-Item $Destination).Length
if ($startBytes -gt 0) {
Write-Host "Partial file found: $([math]::Round($startBytes / 1MB, 1)) MB already downloaded"
Write-Host "Resuming..."
Write-Host ""
}
}
# Ensure destination directory exists
$destDir = Split-Path $Destination -Parent
if ($destDir -and !(Test-Path $destDir)) {
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
}
# Build headers
$headers = @{}
if ($startBytes -gt 0) {
$headers["Range"] = "bytes=$startBytes-"
}
# Use Invoke-WebRequest with domain credentials for SharePoint auth
$params = @{
Uri = $Url
OutFile = $Destination
UseDefaultCredentials = $true
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
Headers = $headers
UseBasicParsing = $true
}
# If resuming, we need to handle appending manually
if ($startBytes -gt 0) {
# Download to a temp file, then append
$tempFile = "$Destination.partial"
$params.OutFile = $tempFile
Write-Host "Downloading..."
Invoke-WebRequest @params
# Check if we got actual content
$tempSize = (Get-Item $tempFile).Length
if ($tempSize -eq 0) {
Remove-Item $tempFile -Force
Write-Host ""
Write-Host "WARNING: Server returned 0 bytes. The URL may have expired."
Write-Host "Get a fresh SharePoint link and try again."
exit 1
}
# Append to existing file
$existingBytes = [System.IO.File]::ReadAllBytes($Destination)
$newBytes = [System.IO.File]::ReadAllBytes($tempFile)
$combined = New-Object byte[] ($existingBytes.Length + $newBytes.Length)
[System.Buffer]::BlockCopy($existingBytes, 0, $combined, 0, $existingBytes.Length)
[System.Buffer]::BlockCopy($newBytes, 0, $combined, $existingBytes.Length, $newBytes.Length)
[System.IO.File]::WriteAllBytes($Destination, $combined)
Remove-Item $tempFile -Force
$totalMB = [math]::Round(($existingBytes.Length + $newBytes.Length) / 1MB, 1)
Write-Host ""
Write-Host "Download complete: $totalMB MB saved to $Destination"
}
else {
Write-Host "Downloading..."
Invoke-WebRequest @params
# Verify we got actual content
$fileSize = (Get-Item $Destination).Length
if ($fileSize -eq 0) {
Remove-Item $Destination -Force
Write-Host ""
Write-Host "WARNING: Downloaded 0 bytes. Possible causes:"
Write-Host " - SharePoint URL expired or requires browser login"
Write-Host " - URL is a redirect/login page, not the actual file"
Write-Host ""
Write-Host "Try this: In Edge, start the download, then go to"
Write-Host "edge://downloads and copy the source URL from there."
exit 1
}
$totalMB = [math]::Round($fileSize / 1MB, 1)
Write-Host ""
Write-Host "Download complete: $totalMB MB saved to $Destination"
}
} catch {
Write-Host ""
Write-Host "Error: $_"
Write-Host ""
if (Test-Path $Destination) {
$partialMB = [math]::Round((Get-Item $Destination).Length / 1MB, 1)
Write-Host "Partial file kept: $partialMB MB"
}
Write-Host "Re-run with the same arguments to resume."
exit 1
}

View File

@@ -0,0 +1,32 @@
@echo off
REM Run-UpdateDNCMXHosts.bat
REM Updates FtpHostPrimary/Secondary in DNC\MX registry on remote shopfloor PCs
REM Usage:
REM Run-UpdateDNCMXHosts.bat -> runs on all PCs in shopfloor-pcs.txt
REM Run-UpdateDNCMXHosts.bat G5N9PWM3ESF -> runs on a single PC (for testing)
echo ============================================================
echo UpdateDNCMXHosts - FTP Host Migration
echo tsgwp00525.us.ae.ge.com -^> tsgwp00525.wjs.geaerospace.net
echo ============================================================
echo.
if not "%~1"=="" (
echo Target: %~1
echo.
powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerName "%~1" -Task UpdateDNCMXHosts -LogFile
) else (
if not exist "%~dp0shopfloor-pcs.txt" (
echo ERROR: shopfloor-pcs.txt not found in %~dp0
echo Either place shopfloor-pcs.txt in the same folder or pass a PC name:
echo Run-UpdateDNCMXHosts.bat G5N9PWM3ESF
pause
exit /b 1
)
echo Target: shopfloor-pcs.txt
echo.
powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerListFile "%~dp0shopfloor-pcs.txt" -Task UpdateDNCMXHosts -LogFile
)
echo.
pause

View File

@@ -68,11 +68,41 @@ param(
[string]$Password
)
$credFile = Join-Path $PSScriptRoot ".maintenance-cred.xml"
$credDir = Join-Path $PSScriptRoot ".creds"
$keyFile = Join-Path $credDir "aes.key"
$userFile = Join-Path $credDir "username.txt"
$passFile = Join-Path $credDir "password.txt"
$scriptDir = $PSScriptRoot
# ---------------------------------------------------------------------------
# Save credentials (DPAPI - only decryptable by this user on this machine)
# Helper: load or create AES key (works across all user contexts on this PC)
# ---------------------------------------------------------------------------
function Get-AESKey {
if (-not (Test-Path $credDir)) {
New-Item -Path $credDir -ItemType Directory -Force | Out-Null
}
if (Test-Path $keyFile) {
return [byte[]](Get-Content $keyFile)
}
$key = New-Object byte[] 32
[System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key)
$key | Set-Content $keyFile
return $key
}
function Load-SavedCredential {
if (-not (Test-Path $userFile) -or -not (Test-Path $passFile)) {
return $null
}
$key = Get-AESKey
$user = Get-Content $userFile
$encPass = Get-Content $passFile
$secPass = $encPass | ConvertTo-SecureString -Key $key
return New-Object System.Management.Automation.PSCredential($user, $secPass)
}
# ---------------------------------------------------------------------------
# Save credentials (AES key file - works from any user context on this PC)
# ---------------------------------------------------------------------------
if ($SaveCredential) {
if ($Username -and $Password) {
@@ -87,9 +117,11 @@ if ($SaveCredential) {
Write-Host "No credentials provided. Exiting." -ForegroundColor Red
exit 1
}
$cred | Export-Clixml -Path $credFile
Write-Host "Credentials saved to: $credFile" -ForegroundColor Green
Write-Host "Encrypted with DPAPI - only YOUR user account on THIS machine can decrypt them." -ForegroundColor Yellow
$key = Get-AESKey
$cred.UserName | Set-Content $userFile
$cred.Password | ConvertFrom-SecureString -Key $key | Set-Content $passFile
Write-Host "Credentials saved to: $credDir" -ForegroundColor Green
Write-Host "Encrypted with AES-256 key - works from any user context (normal or admin)." -ForegroundColor Yellow
Write-Host ""
Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan
Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot"
@@ -113,7 +145,7 @@ if ($CreateScheduledTask) {
$absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path
$absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1"
if (-not (Test-Path $credFile)) {
if (-not (Test-Path $passFile)) {
Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red
exit 1
}
@@ -177,20 +209,13 @@ if (-not $ComputerListFile) {
}
# Load saved credentials
if (-not (Test-Path $credFile)) {
Write-Host "ERROR: No saved credentials found at $credFile" -ForegroundColor Red
$cred = Load-SavedCredential
if (-not $cred) {
Write-Host "ERROR: No saved credentials found in $credDir" -ForegroundColor Red
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
exit 1
}
try {
$cred = Import-Clixml -Path $credFile
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
} catch {
Write-Host "ERROR: Failed to load credentials: $_" -ForegroundColor Red
Write-Host "Re-run with -SaveCredential to save new credentials." -ForegroundColor Yellow
exit 1
}
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
# Log output
$logDir = Join-Path $scriptDir "logs"
@@ -202,7 +227,41 @@ Write-Host "Log: $logFile" -ForegroundColor Gray
$mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1"
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred 2>&1 | Tee-Object -FilePath $logFile
Start-Transcript -Path $logFile -Force | Out-Null
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred
Stop-Transcript | Out-Null
# Parse log for results summary
$logContent = Get-Content $logFile -ErrorAction SilentlyContinue
$okPCs = @($logContent | Select-String '\[OK\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
$failPCs = @($logContent | Select-String '\[FAIL\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Where-Object { $_ -ne ':' } | Sort-Object -Unique)
$summaryFile = Join-Path $logDir "LAST-RUN-SUMMARY.txt"
$summary = @()
$summary += "============================================"
$summary += " MAINTENANCE RESULTS - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
$summary += " Task: $Task"
$summary += "============================================"
$summary += ""
$summary += "SUCCEEDED: $($okPCs.Count)"
foreach ($pc in $okPCs) { $summary += " [OK] $pc" }
$summary += ""
$summary += "FAILED: $($failPCs.Count)"
foreach ($pc in $failPCs) { $summary += " [FAIL] $pc" }
$summary += ""
$summary += "Full log: $logFile"
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
Write-Host ""
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green
Write-Host ""
Write-Host "=== RESULTS ===" -ForegroundColor White
Write-Host " Succeeded: $($okPCs.Count)" -ForegroundColor Green
foreach ($pc in $okPCs) { Write-Host " $pc" -ForegroundColor Green }
if ($failPCs.Count -gt 0) {
Write-Host " Failed: $($failPCs.Count)" -ForegroundColor Red
foreach ($pc in $failPCs) { Write-Host " $pc" -ForegroundColor Red }
}
Write-Host ""
Write-Host "Summary also saved to: $summaryFile" -ForegroundColor Yellow

View File

@@ -0,0 +1,279 @@
# ============================================
# UDC Application Update Script (PowerShell)
# ============================================
param(
[string]$Version
)
# Set error action preference
$ErrorActionPreference = "Stop"
# Set variables
$UDC_PATH = "C:\Program Files\UDC"
$VERSION_FILE = "S:\SPC\UDC\UDC_Update.txt"
$LOG_DIR = "S:\DT\Cameron\UDC\logs"
# Get hostname and timestamp
$HOSTNAME = $env:COMPUTERNAME
$TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss"
$LOG_FILE = "$LOG_DIR\logs_$($HOSTNAME)_$TIMESTAMP.txt"
# Flag to track if we should skip the update
$SKIP_UPDATE = $false
# Function to write logs
function Write-Log {
param([string]$Message)
$LogMessage = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
Write-Host $Message
Add-Content -Path $LOG_FILE -Value $LogMessage -ErrorAction SilentlyContinue
}
# Create log directory if needed
try {
if (-not (Test-Path $LOG_DIR)) {
New-Item -Path $LOG_DIR -ItemType Directory -Force | Out-Null
}
} catch {
Write-Host "WARNING: Cannot access log directory $LOG_DIR"
Write-Host "Update will continue but logging is disabled."
}
Write-Log "============================================"
Write-Log "UDC Update Script Started on $HOSTNAME"
Write-Log "============================================"
# ============================================
# Step 1: Check if PPMon.exe or ppdcs.exe are running
# ============================================
Write-Host "Checking for conflicting processes..."
$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue
if ($conflictingProcesses) {
foreach ($proc in $conflictingProcesses) {
Write-Log "$($proc.Name).exe is running. Skipping update process."
Write-Host "$($proc.Name).exe is running. Update will be skipped."
}
$SKIP_UPDATE = $true
}
# ============================================
# Step 2: Check if UDC directory exists
# ============================================
Write-Host "Checking if UDC is installed..."
if (-not (Test-Path $UDC_PATH)) {
Write-Log "UDC directory not found. Exiting."
Write-Host "UDC is not installed on this machine."
exit 0
}
# ============================================
# Step 3: Check if UDC.exe exists
# ============================================
$UDC_EXE = "$UDC_PATH\UDC.exe"
if (-not (Test-Path $UDC_EXE)) {
Write-Log "UDC.exe not found. Exiting."
Write-Host "UDC.exe not found in installation directory."
exit 0
}
# ============================================
# ONLY PROCEED WITH UPDATE IF NOT SKIPPED
# ============================================
if (-not $SKIP_UPDATE) {
# ============================================
# Step 4: Determine target version
# ============================================
if ($Version) {
$NETWORK_VERSION = $Version
Write-Host "Using override version: $NETWORK_VERSION"
Write-Log "Override version specified: $NETWORK_VERSION"
} else {
if (-not (Test-Path $VERSION_FILE)) {
Write-Log "Version file not found: $VERSION_FILE"
Write-Host "ERROR: Version file not found."
exit 1
}
Write-Host "Reading version information..."
try {
$versionContent = Get-Content $VERSION_FILE | Where-Object { $_ -match "Version:" }
$NETWORK_VERSION = ($versionContent -split ":")[1].Trim()
if ([string]::IsNullOrEmpty($NETWORK_VERSION)) {
throw "Could not parse network version"
}
Write-Host "Network version: $NETWORK_VERSION"
Write-Log "Network version: $NETWORK_VERSION"
} catch {
Write-Log "Could not read network version. Error: $_"
Write-Host "ERROR: Could not determine network version."
exit 1
}
}
# Build source path from version
$SOURCE_PATH = "S:\SPC\UDC\UDC_$NETWORK_VERSION"
# ============================================
# Step 5: Check if source files exist
# ============================================
if (-not (Test-Path $SOURCE_PATH)) {
Write-Log "Source path not found: $SOURCE_PATH"
Write-Host "ERROR: Update source files not found at $SOURCE_PATH"
exit 1
}
# ============================================
# Step 6: Get local UDC.exe version
# ============================================
try {
$fileVersion = (Get-Item $UDC_EXE).VersionInfo.FileVersion
# Trim to 3 parts if it has 4 (e.g., 1.0.30.0 -> 1.0.30)
$versionParts = $fileVersion.Split('.')
if ($versionParts.Count -eq 4) {
$LOCAL_VERSION = "$($versionParts[0]).$($versionParts[1]).$($versionParts[2])"
} else {
$LOCAL_VERSION = $fileVersion
}
Write-Host "Local version: $LOCAL_VERSION"
Write-Log "Local version: $LOCAL_VERSION"
} catch {
Write-Log "Could not read local version. Error: $_"
Write-Host "ERROR: Could not determine local version."
exit 1
}
# ============================================
# Step 7: Compare versions
# ============================================
try {
$netVer = [version]$NETWORK_VERSION
$localVer = [version]$LOCAL_VERSION
if ($netVer -eq $localVer) {
Write-Log "Versions match ($LOCAL_VERSION). No update needed."
Write-Host "Version is current. No update required."
$SKIP_UPDATE = $true
}
elseif ($netVer -le $localVer) {
Write-Log "Network version ($NETWORK_VERSION) not newer than local ($LOCAL_VERSION)."
Write-Host "Local version is current or newer. No update required."
$SKIP_UPDATE = $true
}
else {
Write-Host "Update required: $LOCAL_VERSION -> $NETWORK_VERSION"
Write-Log "Update required: $LOCAL_VERSION -> $NETWORK_VERSION"
}
} catch {
Write-Log "Error comparing versions. Error: $_"
Write-Host "ERROR: Could not compare versions."
exit 1
}
# ============================================
# Step 8: Perform update if needed
# ============================================
if (-not $SKIP_UPDATE) {
# Kill UDC.exe if running
Write-Host "Checking if UDC.exe is running..."
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
if ($udcProcess) {
Write-Host "UDC.exe is running. Stopping process..."
Write-Log "Killing UDC.exe"
try {
Stop-Process -Name "UDC" -Force
Start-Sleep -Seconds 2
Write-Log "UDC.exe stopped successfully"
} catch {
Write-Log "Warning: Could not stop UDC.exe. Error: $_"
Write-Host "WARNING: Could not stop UDC.exe"
}
}
# Copy update files
Write-Host "Copying update files..."
Write-Log "Copying files from $SOURCE_PATH to $UDC_PATH"
try {
# Use robocopy for better performance and logging
$robocopyArgs = @(
$SOURCE_PATH,
$UDC_PATH,
"/E", # Copy subdirectories including empty ones
"/NFL", # No file list
"/NDL", # No directory list
"/NJH", # No job header
"/NJS", # No job summary
"/NC", # No class
"/NS", # No size
"/NP" # No progress
)
$result = robocopy @robocopyArgs
# Robocopy exit codes: 0-7 are success, 8+ are errors
if ($LASTEXITCODE -ge 8) {
throw "Robocopy failed with exit code $LASTEXITCODE"
}
Write-Host "Files copied successfully."
Write-Log "Files copied successfully."
} catch {
Write-Log "ERROR: File copy failed. Error: $_"
Write-Host "ERROR: Failed to copy update files."
exit 1
}
}
}
# ============================================
# Step 9: Ensure UDC.exe is running (ONLY if no conflicting processes)
# ============================================
# Re-check for conflicting processes before starting UDC
$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue
if ($conflictingProcesses) {
Write-Log "Conflicting processes still running. Will not start UDC.exe"
Write-Host "Conflicting processes detected. UDC.exe will not be started."
} else {
Write-Host "Verifying UDC.exe is running..."
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
if (-not $udcProcess) {
Write-Host "UDC.exe is not running. Starting it now..."
Write-Log "UDC.exe not running. Starting UDC.exe"
try {
Start-Process -FilePath $UDC_EXE
Start-Sleep -Seconds 2
# Verify it started
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
if ($udcProcess) {
Write-Log "UDC.exe started successfully."
Write-Host "UDC.exe started successfully!"
} else {
Write-Log "WARNING: UDC.exe may not have started."
Write-Host "WARNING: UDC.exe may not have started properly."
}
} catch {
Write-Log "WARNING: Could not start UDC.exe. Error: $_"
Write-Host "WARNING: Could not start UDC.exe"
}
} else {
Write-Log "UDC.exe is already running."
Write-Host "UDC.exe is already running."
}
}
Write-Log "UDC Update Script Completed"
Write-Log "============================================"
exit 0

View File

@@ -0,0 +1,8 @@
@echo off
:: ============================================
:: UDC Update PowerShell Launcher
:: ============================================
powershell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""U:/Scripts/UDC/UDC_Update.ps1""' -Verb RunAs}"
exit /b %ERRORLEVEL%

View File

@@ -0,0 +1,11 @@
@echo off
:: ============================================
:: UDC Update Override Launcher
:: Edit the version below to force a specific update
:: ============================================
set "UDC_VERSION=1.0.32"
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& 'U:/Scripts/UDC/UDC_Update.ps1' -Version '%UDC_VERSION%'"
exit /b %ERRORLEVEL%

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