Adds the Blueforce Field OS provisioning stack on top of the existing fleet architecture, without rewriting the working systems (installer, diagnostics, Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus). ISO build (the previous builder was a skeleton that produced no medium): - boot layout is read from the base ISO at build time via `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`; `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2 MBR lives in the source image's first 16 sectors and the EFI system partition is an appended partition, neither of which is a file in the ISO9660 tree - preflight now asserts El Torito boot metadata positively: xorriso exits 0 on a non-ISO input, which previously let an arbitrary file pass --check - artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso, 6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved, /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical to the base - recorded baseline deviation: the verified medium on the build host is the Desktop flavour, so the manifest carries a baseline_deviation block instead of hiding it; Server remains one flag away Offline and identity: - offline APT contract with concrete package requirements, pin format and an audit table; builds fail closed while pins are UNPINNED - autoinstall keeps interactive-sections: [storage]: the operator confirms the target disk, and no automatic wipe is ever performed - firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a missing internet link or WireGuard handshake cannot fail provisioning but can never produce a false READY Fleet and migration: - five read-only Ansible playbooks for Field OS version, provisioning, enrollment, remote channels and offline readiness, all wave-scoped with a separate production override - Windows pre-migration inventory and data export extended to the full brief scope, read-only, never collecting credentials Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
1070 lines
48 KiB
PowerShell
1070 lines
48 KiB
PowerShell
<#
|
||
.SYNOPSIS
|
||
Read-only Windows pre-migration inventory for one Blueforce dealer.
|
||
.DESCRIPTION
|
||
Collects the full pre-migration picture of a Windows field terminal:
|
||
hardware, Windows version, disks/volumes, network (adapters, IP addresses,
|
||
DNS, gateways, routes, network profiles, proxy), serial/COM ports, USB
|
||
devices and hardware IDs, drivers, installed software, services, scheduled
|
||
tasks, Docker presence, VPN clients (WireGuard/OpenVPN/other), RustDesk and
|
||
AnyDesk device IDs, MEG application/service/configuration paths, and the
|
||
current Windows hostname.
|
||
|
||
READ-ONLY CONTRACT (asserted by tests/check-migration-static.sh):
|
||
* No Set-*, New-*, Remove-* cmdlets, no service control, no network
|
||
configuration command, no registry write, no download.
|
||
* No credential, password, token or private key is collected.
|
||
* The registry is only ever read (Get-ItemProperty / Get-ChildItem).
|
||
* RustDesk/AnyDesk: only the numeric device ID value is extracted. The
|
||
RustDesk id_ed25519 private key and the contents of *.conf, *.ovpn and
|
||
*.dpapi files are never read - only their file names are listed.
|
||
* No external tool is executed (docker.exe included); all data comes from
|
||
CIM, the registry and file/directory metadata.
|
||
* The only writes are the JSON and Markdown reports under
|
||
-OutputDirectory; with -Check nothing is written at all.
|
||
|
||
Output contract: BF-<no>-windows-inventory.json and
|
||
BF-<no>-windows-inventory.md, where <no> is the 8-digit dealer number - not
|
||
the hostname of the machine being inventoried.
|
||
.PARAMETER DealerId
|
||
8-digit Blueforce dealer number (^[0-9]{8}$). Device ID becomes BF-<no>.
|
||
.PARAMETER DealerIdUnknown
|
||
Explicit opt-in for a device whose dealer number is not known yet. The report
|
||
is written as BF-unknown-windows-inventory.* and carries a prominent warning:
|
||
it must not be uploaded to the central inventory until the dealer number is
|
||
confirmed and the script is re-run with -DealerId.
|
||
.PARAMETER OutputDirectory
|
||
Directory that receives BF-<no>-windows-inventory.json and .md.
|
||
.PARAMETER NoSoftware
|
||
Skip the software surface (installed software, services, scheduled tasks,
|
||
drivers). Hardware, network, device and MEG-path sections are still collected.
|
||
.PARAMETER Check
|
||
Dry run: validate parameters, print the collection plan and the resolved
|
||
output paths, write nothing, exit 0.
|
||
.EXAMPLE
|
||
.\BF-WindowsPreMigrationInventory.ps1 -DealerId 12010193 -OutputDirectory C:\Blueforce\migration
|
||
.EXAMPLE
|
||
.\BF-WindowsPreMigrationInventory.ps1 -DealerIdUnknown -OutputDirectory C:\Blueforce\migration
|
||
.EXAMPLE
|
||
.\BF-WindowsPreMigrationInventory.ps1 -DealerId 12010193 -OutputDirectory C:\Blueforce\migration -Check
|
||
.NOTES
|
||
Elevation is not required and nothing is changed. Some CIM sections can be
|
||
incomplete when the script runs unelevated; that is reported in "notes".
|
||
#>
|
||
[CmdletBinding(DefaultParameterSetName = 'KnownDealer')]
|
||
param(
|
||
[Parameter(Mandatory, ParameterSetName = 'KnownDealer')]
|
||
[ValidatePattern('^[0-9]{8}$')]
|
||
[string]$DealerId,
|
||
|
||
[Parameter(Mandatory, ParameterSetName = 'UnknownDealer')]
|
||
[switch]$DealerIdUnknown,
|
||
|
||
[Parameter(Mandatory)]
|
||
[ValidateNotNullOrEmpty()]
|
||
[string]$OutputDirectory,
|
||
|
||
[switch]$NoSoftware,
|
||
|
||
[switch]$Check
|
||
)
|
||
|
||
Set-StrictMode -Version Latest
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
function Get-SafeValue {
|
||
param([object]$Value)
|
||
if ($null -eq $Value) { return 'unknown' }
|
||
$text = ([string]$Value).Trim()
|
||
if ([string]::IsNullOrWhiteSpace($text)) { return 'unknown' }
|
||
return $text
|
||
}
|
||
|
||
function ConvertTo-MarkdownCell {
|
||
param([object]$Value)
|
||
return ((Get-SafeValue $Value) -replace '\|', '\|' -replace "`r?`n", '; ')
|
||
}
|
||
|
||
function ConvertTo-Gigabytes {
|
||
param([object]$Bytes)
|
||
if ($null -eq $Bytes) { return 'unknown' }
|
||
return [math]::Round(([double]$Bytes) / 1GB, 2)
|
||
}
|
||
|
||
function ConvertTo-Megabytes {
|
||
param([object]$Bytes)
|
||
if ($null -eq $Bytes) { return 'unknown' }
|
||
return [long][math]::Round(([double]$Bytes) / 1MB)
|
||
}
|
||
|
||
function ConvertTo-IsoUtc {
|
||
param([object]$Value)
|
||
if ($null -eq $Value) { return 'unknown' }
|
||
try { return ([datetime]$Value).ToUniversalTime().ToString('o') } catch { return 'unknown' }
|
||
}
|
||
|
||
# Null-safe property read for CIM/WMI objects, so a missing class or a failed
|
||
# probe degrades to $null/'unknown' instead of aborting the report.
|
||
function Get-ObjectValue {
|
||
param([object]$InputObject, [string]$Name)
|
||
if ($null -eq $InputObject) { return $null }
|
||
$prop = $InputObject.PSObject.Properties | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
|
||
if ($null -eq $prop) { return $null }
|
||
return $prop.Value
|
||
}
|
||
|
||
function Get-ProgramFilesRoots {
|
||
$roots = @()
|
||
if ($env:ProgramFiles) { $roots += $env:ProgramFiles }
|
||
if (${env:ProgramFiles(x86)}) { $roots += ${env:ProgramFiles(x86)} }
|
||
if ($env:ProgramData) { $roots += $env:ProgramData }
|
||
$roots += 'C:\'
|
||
return @($roots | Select-Object -Unique)
|
||
}
|
||
|
||
# Reads named values from a registry key. Read-only; missing values stay
|
||
# 'unknown' and never abort the report.
|
||
function Get-RegistryValues {
|
||
param([string]$Path, [string[]]$Names)
|
||
$result = [ordered]@{}
|
||
foreach ($name in $Names) { $result[$name] = 'unknown' }
|
||
if (-not (Test-Path -LiteralPath $Path)) { return $result }
|
||
$props = Get-ItemProperty -LiteralPath $Path -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { return $result }
|
||
foreach ($name in $Names) {
|
||
$prop = $props.PSObject.Properties | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
||
if ($null -ne $prop) { $result[$name] = Get-SafeValue $prop.Value }
|
||
}
|
||
return $result
|
||
}
|
||
|
||
# Reads only the registry values whose NAME matches an exact allow-list
|
||
# pattern. Used to pick up path-like settings without ever touching
|
||
# credential/secret values that live in the same key.
|
||
function Get-RegistryValuesByNamePattern {
|
||
param([string]$Path, [string]$NamePattern)
|
||
$result = [ordered]@{}
|
||
if (-not (Test-Path -LiteralPath $Path)) { return $result }
|
||
$props = Get-ItemProperty -LiteralPath $Path -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { return $result }
|
||
foreach ($prop in $props.PSObject.Properties) {
|
||
if ($prop.Name -match $NamePattern) { $result[$prop.Name] = Get-SafeValue $prop.Value }
|
||
}
|
||
return $result
|
||
}
|
||
|
||
# Extracts a single captured value from one line of a configuration file.
|
||
# The file content is never returned, stored or reported - only the capture
|
||
# group. This helper is NEVER called on key material (RustDesk id_ed25519,
|
||
# *.ovpn, *.dpapi).
|
||
function Get-ConfigFileValue {
|
||
param([string]$Path, [string]$LinePattern, [string]$CapturePattern)
|
||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return 'not-found' }
|
||
$hit = Select-String -LiteralPath $Path -Pattern $LinePattern -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
if ($null -eq $hit) { return 'not-found' }
|
||
if ($hit.Line -match $CapturePattern) { return (Get-SafeValue $Matches[1]) }
|
||
return 'not-found'
|
||
}
|
||
|
||
function Get-FileVersionInfo {
|
||
param([string]$Path)
|
||
if (-not $Path) { return 'unknown' }
|
||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return 'not-found' }
|
||
$item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
|
||
if ($null -eq $item) { return 'unknown' }
|
||
if ($null -eq $item.VersionInfo) { return 'unknown' }
|
||
return (Get-SafeValue $item.VersionInfo.FileVersion)
|
||
}
|
||
|
||
# Directory metadata (path + mtime) only; no file contents.
|
||
function Get-DirectoryReport {
|
||
param([string]$Root, [string]$Filter)
|
||
if ([string]::IsNullOrWhiteSpace($Root)) { return @() }
|
||
if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return @() }
|
||
return @(Get-ChildItem -LiteralPath $Root -Directory -Filter $Filter -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
path = $_.FullName
|
||
last_write_utc = ConvertTo-IsoUtc $_.LastWriteTimeUtc
|
||
review_required = $true
|
||
}
|
||
})
|
||
}
|
||
|
||
# File NAMES only for review lists such as VPN tunnel configs (contents are
|
||
# credentials/keys and are deliberately never read).
|
||
function Get-FileNameReport {
|
||
param([string[]]$Roots, [string]$Filter)
|
||
$out = @()
|
||
foreach ($root in $Roots) {
|
||
if ([string]::IsNullOrWhiteSpace($root)) { continue }
|
||
if (-not (Test-Path -LiteralPath $root -PathType Container)) { continue }
|
||
$out += @(Get-ChildItem -LiteralPath $root -File -Filter $Filter -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
name = $_.Name
|
||
path = $_.FullName
|
||
last_write_utc = ConvertTo-IsoUtc $_.LastWriteTimeUtc
|
||
contents_read = $false
|
||
}
|
||
})
|
||
}
|
||
return @($out)
|
||
}
|
||
|
||
function Get-NamesOf {
|
||
param([object[]]$Items)
|
||
return @($Items | ForEach-Object { $_['name'] })
|
||
}
|
||
|
||
function Get-InstalledSoftware {
|
||
$uninstallPaths = @(
|
||
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
||
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
||
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||
)
|
||
$items = Get-ItemProperty -Path $uninstallPaths -ErrorAction SilentlyContinue |
|
||
Where-Object { -not [string]::IsNullOrWhiteSpace($_.DisplayName) } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.DisplayName
|
||
version = Get-SafeValue $_.DisplayVersion
|
||
publisher = Get-SafeValue $_.Publisher
|
||
install_date = Get-SafeValue $_.InstallDate
|
||
install_location = Get-SafeValue $_.InstallLocation
|
||
}
|
||
}
|
||
return @($items | Sort-Object -Property { $_['name'] })
|
||
}
|
||
|
||
# Running services, auto-start services that are NOT running (a migration
|
||
# signal), and the subset relevant to remote access / Docker / VPN / MEG.
|
||
function Get-ServiceSnapshot {
|
||
$watchlist = '(?i)docker|wireguard|openvpn|rustdesk|anydesk|mesh|meg|blueforce|teamviewer|tailscale|zerotier|forti|anyconnect|globalprotect|vpn'
|
||
$rows = @(Get-CimInstance -ClassName Win32_Service -ErrorAction SilentlyContinue | ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.Name
|
||
display_name = Get-SafeValue $_.DisplayName
|
||
state = Get-SafeValue $_.State
|
||
start_mode = Get-SafeValue $_.StartMode
|
||
start_name = Get-SafeValue $_.StartName
|
||
path = Get-SafeValue $_.PathName
|
||
}
|
||
})
|
||
$running = @($rows | Where-Object { $_['state'] -eq 'Running' } | Sort-Object -Property { $_['name'] })
|
||
$stoppedAuto = @($rows | Where-Object { $_['state'] -eq 'Stopped' -and $_['start_mode'] -eq 'Auto' } | Sort-Object -Property { $_['name'] })
|
||
$relevant = @($rows | Where-Object { ($_['name'] + ' ' + $_['display_name']) -match $watchlist } | Sort-Object -Property { $_['name'] })
|
||
return [ordered]@{
|
||
total_services = $rows.Count
|
||
selection = 'running + auto-start-but-stopped + migration-relevant; a dump of every stopped/disabled service is intentionally omitted to keep the report bounded'
|
||
running = $running
|
||
auto_start_but_stopped = $stoppedAuto
|
||
relevant_to_migration = $relevant
|
||
}
|
||
}
|
||
|
||
# Non-Microsoft scheduled tasks (vendor tasks) plus the migration-relevant
|
||
# subset. Uses the ScheduledTasks module only; no other execution path.
|
||
function Get-ScheduledTaskSnapshot {
|
||
$watchlist = '(?i)docker|meg|blueforce|vpn|wireguard|openvpn|rustdesk|anydesk|mesh|backup|veeam|acronis'
|
||
$tasks = @()
|
||
try {
|
||
$tasks = @(Get-ScheduledTask -ErrorAction Stop |
|
||
Where-Object { $_.TaskPath -notlike '\Microsoft\*' } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
task_path = Get-SafeValue $_.TaskPath
|
||
task_name = Get-SafeValue $_.TaskName
|
||
state = Get-SafeValue ([string]$_.State)
|
||
author = Get-SafeValue $_.Author
|
||
run_as_user = Get-SafeValue $_.Principal.UserId
|
||
}
|
||
})
|
||
} catch {
|
||
return [ordered]@{
|
||
available = $false
|
||
note = 'Get-ScheduledTask unavailable on this host; scheduled-task enumeration skipped (no other tool is invoked).'
|
||
total = 0
|
||
items = @()
|
||
relevant_to_migration = @()
|
||
}
|
||
}
|
||
$items = @($tasks | Sort-Object -Property { $_['task_path'] + $_['task_name'] })
|
||
return [ordered]@{
|
||
available = $true
|
||
note = 'Non-Microsoft task paths only.'
|
||
total = $items.Count
|
||
items = $items
|
||
relevant_to_migration = @($items | Where-Object { ($_['task_path'] + $_['task_name']) -match $watchlist })
|
||
}
|
||
}
|
||
|
||
# COM/serial ports from three read-only sources: CIM, the SERIALCOMM device
|
||
# map, and PnP entities whose friendly name carries a (COMx) suffix.
|
||
function Get-ComPortSnapshot {
|
||
$cimPorts = @(Get-CimInstance -ClassName Win32_SerialPort -ErrorAction SilentlyContinue | ForEach-Object {
|
||
[ordered]@{
|
||
device_id = Get-SafeValue $_.DeviceID
|
||
name = Get-SafeValue $_.Name
|
||
description = Get-SafeValue $_.Description
|
||
provider_type = Get-SafeValue $_.ProviderType
|
||
pnp_device_id = Get-SafeValue $_.PNPDeviceID
|
||
}
|
||
})
|
||
$deviceMap = [ordered]@{}
|
||
if (Test-Path -LiteralPath 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM') {
|
||
$props = Get-ItemProperty -LiteralPath 'HKLM:\HARDWARE\DEVICEMAP\SERIALCOMM' -ErrorAction SilentlyContinue
|
||
if ($null -ne $props) {
|
||
foreach ($prop in $props.PSObject.Properties) {
|
||
if ($prop.Name -like 'PS*') { continue }
|
||
$deviceMap[$prop.Name] = Get-SafeValue $prop.Value
|
||
}
|
||
}
|
||
}
|
||
$pnpPorts = @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.Name -like '*(COM*' } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.Name
|
||
device_id = Get-SafeValue $_.DeviceID
|
||
status = Get-SafeValue $_.Status
|
||
}
|
||
})
|
||
return [ordered]@{
|
||
cim_serial_ports = $cimPorts
|
||
registry_devicemap = $deviceMap
|
||
pnp_com_devices = $pnpPorts
|
||
}
|
||
}
|
||
|
||
# USB devices with their hardware IDs (VID_/PID_ live in DeviceID) plus the
|
||
# USB host controllers.
|
||
function Get-UsbSnapshot {
|
||
$devices = @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.DeviceID -like 'USB\*' -or $_.PNPClass -eq 'USB' } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.Name
|
||
description = Get-SafeValue $_.Description
|
||
hardware_id = Get-SafeValue $_.DeviceID
|
||
device_class = Get-SafeValue $_.PNPClass
|
||
status = Get-SafeValue $_.Status
|
||
service = Get-SafeValue $_.Service
|
||
}
|
||
})
|
||
$controllers = @(Get-CimInstance -ClassName Win32_USBController -ErrorAction SilentlyContinue | ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.Name
|
||
device_id = Get-SafeValue $_.DeviceID
|
||
}
|
||
})
|
||
return [ordered]@{
|
||
devices = @($devices | Sort-Object -Property { $_['name'] })
|
||
controllers = $controllers
|
||
}
|
||
}
|
||
|
||
function Get-DriverSnapshot {
|
||
$drivers = @(Get-CimInstance -ClassName Win32_PnPSignedDriver -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.DeviceName } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
device_name = Get-SafeValue $_.DeviceName
|
||
device_class = Get-SafeValue $_.DeviceClass
|
||
manufacturer = Get-SafeValue $_.Manufacturer
|
||
driver_provider = Get-SafeValue $_.DriverProviderName
|
||
driver_version = Get-SafeValue $_.DriverVersion
|
||
driver_date = ConvertTo-IsoUtc $_.DriverDate
|
||
inf_name = Get-SafeValue $_.InfName
|
||
}
|
||
})
|
||
return [ordered]@{
|
||
total = $drivers.Count
|
||
items = @($drivers | Sort-Object -Property { $_['device_name'] })
|
||
}
|
||
}
|
||
|
||
# Docker presence from install metadata and service state only. docker.exe is
|
||
# never executed: no engine contact, no image/build/container side effect.
|
||
function Get-DockerSnapshot {
|
||
$roots = Get-ProgramFilesRoots
|
||
$dockerCli = 'unknown'
|
||
$cmd = Get-Command -Name 'docker.exe' -ErrorAction SilentlyContinue
|
||
if ($null -ne $cmd) { $dockerCli = Get-SafeValue (Get-ObjectValue $cmd 'Source') }
|
||
|
||
$desktopApp = 'not-found'
|
||
foreach ($root in $roots) {
|
||
$candidate = Join-Path $root 'Docker\Docker\Docker Desktop.exe'
|
||
if (Test-Path -LiteralPath $candidate -PathType Leaf) { $desktopApp = $candidate; break }
|
||
}
|
||
|
||
$service = $null
|
||
$svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='com.docker.service'" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
if ($null -ne $svc) {
|
||
$service = [ordered]@{
|
||
name = Get-SafeValue (Get-ObjectValue $svc 'Name')
|
||
state = Get-SafeValue (Get-ObjectValue $svc 'State')
|
||
start_mode = Get-SafeValue (Get-ObjectValue $svc 'StartMode')
|
||
}
|
||
}
|
||
|
||
$wslDistros = @()
|
||
if (Test-Path -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss') {
|
||
$wslDistros = @(Get-ChildItem -LiteralPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss' -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
$props = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { return }
|
||
if (@($props.PSObject.Properties.Name) -contains 'DistributionName') {
|
||
Get-SafeValue $props.DistributionName
|
||
}
|
||
} | Where-Object { $_ })
|
||
}
|
||
|
||
$present = ($dockerCli -ne 'unknown') -or ($desktopApp -ne 'not-found') -or ($null -ne $service)
|
||
return [ordered]@{
|
||
present = [bool]$present
|
||
docker_cli_path = $dockerCli
|
||
docker_cli_file_version = Get-FileVersionInfo $dockerCli
|
||
desktop_app_path = $desktopApp
|
||
desktop_app_file_version = Get-FileVersionInfo $desktopApp
|
||
service = $service
|
||
wsl_distributions = @($wslDistros)
|
||
note = 'Presence and file metadata only; the docker CLI is never executed.'
|
||
}
|
||
}
|
||
|
||
# VPN clients: installed software matches, related services, tunnel/profile
|
||
# NAMES (contents of *.conf.dpapi and *.ovpn are credentials and are not read).
|
||
function Get-VpnSnapshot {
|
||
param([object[]]$Software, [object[]]$Services)
|
||
|
||
$softwarePattern = '(?i)(wireguard|openvpn|tailscale|zerotier|anyconnect|forticlient|globalprotect|softether|nordvpn|protonvpn|expressvpn|surfshark|windscribe)'
|
||
$softwareMatches = @($Software | Where-Object { ($_['name'] + ' ' + $_['publisher']) -match $softwarePattern })
|
||
$servicePattern = '(?i)wireguard|openvpn|tailscale|zerotier|anyconnect|forticlient|globalprotect|vpnagent'
|
||
$serviceMatches = @($Services | Where-Object { ($_['name'] + ' ' + $_['display_name']) -match $servicePattern })
|
||
|
||
$roots = Get-ProgramFilesRoots
|
||
$wireguardDirs = @()
|
||
$openVpnDirs = @()
|
||
foreach ($root in $roots) {
|
||
$wireguardDirs += (Join-Path $root 'WireGuard\Data\Configurations')
|
||
$openVpnDirs += (Join-Path $root 'OpenVPN\config')
|
||
$openVpnDirs += (Join-Path $root 'OpenVPN\config-auto')
|
||
}
|
||
|
||
$wireguardKeys = @()
|
||
if (Test-Path -LiteralPath 'HKLM:\SOFTWARE\WireGuard') {
|
||
$wireguardKeys = @(Get-ChildItem -LiteralPath 'HKLM:\SOFTWARE\WireGuard' -ErrorAction SilentlyContinue |
|
||
ForEach-Object { Get-SafeValue (Get-ObjectValue $_ 'Name') })
|
||
}
|
||
$wireguardTunnels = @(Get-FileNameReport -Roots $wireguardDirs -Filter '*.conf.dpapi')
|
||
$openVpnProfiles = @(Get-FileNameReport -Roots $openVpnDirs -Filter '*.ovpn')
|
||
|
||
$wireguardPresent = (@($softwareMatches | Where-Object { $_['name'] -match '(?i)wireguard' }).Count -gt 0) -or
|
||
($wireguardKeys.Count -gt 0) -or ($wireguardTunnels.Count -gt 0)
|
||
$openVpnPresent = (@($softwareMatches | Where-Object { $_['name'] -match '(?i)openvpn' }).Count -gt 0) -or
|
||
($openVpnProfiles.Count -gt 0)
|
||
|
||
return [ordered]@{
|
||
wireguard = [ordered]@{
|
||
present = [bool]$wireguardPresent
|
||
registry_tunnels = $wireguardKeys
|
||
tunnel_config_files = $wireguardTunnels
|
||
contents_note = 'Tunnel configuration contents are never read; only file names are listed.'
|
||
}
|
||
openvpn = [ordered]@{
|
||
present = [bool]$openVpnPresent
|
||
profile_files = $openVpnProfiles
|
||
registry_paths = Get-RegistryValuesByNamePattern -Path 'HKLM:\SOFTWARE\OpenVPN' -NamePattern '(?i)^(config_dir|config_auto|config|log_dir|exe_path|install_dir)$'
|
||
contents_note = 'Profile contents (certificates/keys) are never read; only file names are listed.'
|
||
}
|
||
installed_vpn_software = $softwareMatches
|
||
vpn_services = $serviceMatches
|
||
}
|
||
}
|
||
|
||
# RustDesk / AnyDesk device IDs. Only the ID value is extracted: for RustDesk
|
||
# the registry value literally named 'id' or the id = ... line of
|
||
# RustDesk.toml; for AnyDesk the ad.anynet.id value. Private-key files
|
||
# (id_ed25519) and password/hash settings are never opened.
|
||
function Get-RemoteAccessSnapshot {
|
||
param([object[]]$Services)
|
||
|
||
$rustdeskId = 'not-found'
|
||
foreach ($path in @('HKCU:\Software\RustDesk', 'HKLM:\Software\RustDesk', 'HKLM:\Software\WOW6432Node\RustDesk')) {
|
||
if ($rustdeskId -ne 'not-found') { break }
|
||
if (-not (Test-Path -LiteralPath $path)) { continue }
|
||
$props = Get-ItemProperty -LiteralPath $path -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { continue }
|
||
$idProp = $props.PSObject.Properties | Where-Object { $_.Name -eq 'id' } | Select-Object -First 1
|
||
if ($null -ne $idProp) { $rustdeskId = Get-SafeValue $idProp.Value }
|
||
}
|
||
if ($rustdeskId -eq 'not-found' -and $env:APPDATA) {
|
||
foreach ($file in @('RustDesk.toml', 'RustDesk2.toml')) {
|
||
$value = Get-ConfigFileValue -Path (Join-Path $env:APPDATA "RustDesk\config\$file") `
|
||
-LinePattern '^\s*id\s*=' -CapturePattern "id\s*=\s*'?([0-9A-Za-z]+)'?"
|
||
if ($value -ne 'not-found') { $rustdeskId = $value; break }
|
||
}
|
||
}
|
||
|
||
$anydeskId = 'not-found'
|
||
foreach ($path in @('HKLM:\Software\AnyDesk', 'HKCU:\Software\AnyDesk', 'HKLM:\Software\WOW6432Node\AnyDesk')) {
|
||
if ($anydeskId -ne 'not-found') { break }
|
||
if (-not (Test-Path -LiteralPath $path)) { continue }
|
||
$props = Get-ItemProperty -LiteralPath $path -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { continue }
|
||
$idProp = $props.PSObject.Properties |
|
||
Where-Object { $_.Name -match '(?i)^ad\.anynet\.id$|^anynet[._]?id$|^client[._]?id$' } |
|
||
Select-Object -First 1
|
||
if ($null -ne $idProp) { $anydeskId = Get-SafeValue $idProp.Value }
|
||
}
|
||
if ($anydeskId -eq 'not-found') {
|
||
$confFiles = @()
|
||
if ($env:ProgramData) { $confFiles += (Join-Path $env:ProgramData 'AnyDesk\system.conf') }
|
||
if ($env:APPDATA) { $confFiles += (Join-Path $env:APPDATA 'AnyDesk\user.conf') }
|
||
foreach ($file in $confFiles) {
|
||
$value = Get-ConfigFileValue -Path $file `
|
||
-LinePattern '^\s*ad\.anynet\.id\s*=' -CapturePattern 'ad\.anynet\.id\s*=\s*([0-9A-Za-z]+)'
|
||
if ($value -ne 'not-found') { $anydeskId = $value; break }
|
||
}
|
||
}
|
||
|
||
$servicePattern = '(?i)rustdesk|anydesk|mesh|teamviewer'
|
||
return [ordered]@{
|
||
rustdesk_id = $rustdeskId
|
||
anydesk_id = $anydeskId
|
||
rustdesk_id_source = 'registry value named id, else the id line of RustDesk.toml (the id_ed25519 private key is never read)'
|
||
anydesk_id_source = 'registry ad.anynet.id, else the ad.anynet.id line of system.conf/user.conf (password/hash settings are never read)'
|
||
remote_access_services = @($Services | Where-Object { ($_['name'] + ' ' + $_['display_name']) -match $servicePattern })
|
||
}
|
||
}
|
||
|
||
# MEG application/service/configuration paths. Directory and registry key
|
||
# NAMES plus path-like registry values only; no directory contents, no
|
||
# arbitrary registry values.
|
||
function Get-MegSnapshot {
|
||
param([object[]]$Software, [object[]]$Services, [string[]]$Roots)
|
||
|
||
$megPattern = '(?i)\bmeg\b'
|
||
$softwareMatches = @($Software | Where-Object { ($_['name'] + ' ' + $_['publisher']) -match $megPattern })
|
||
$serviceMatches = @($Services | Where-Object { ($_['name'] + ' ' + $_['display_name']) -match $megPattern })
|
||
|
||
$directories = @()
|
||
foreach ($root in $Roots) { $directories += @(Get-DirectoryReport -Root $root -Filter '*MEG*') }
|
||
|
||
$registryKeys = @()
|
||
foreach ($base in @('HKLM:\SOFTWARE', 'HKLM:\SOFTWARE\WOW6432Node', 'HKCU:\Software')) {
|
||
if (-not (Test-Path -LiteralPath $base)) { continue }
|
||
$registryKeys += @(Get-ChildItem -LiteralPath $base -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.PSChildName -match $megPattern } |
|
||
ForEach-Object { Get-SafeValue $_.Name })
|
||
}
|
||
$registryKeys = @($registryKeys | Where-Object { $_ -ne 'unknown' } | Select-Object -Unique)
|
||
|
||
$registryPaths = [ordered]@{}
|
||
foreach ($key in $registryKeys) {
|
||
# Registry key names come back as HKEY_LOCAL_MACHINE\...; map them to
|
||
# the PowerShell drive form before reading.
|
||
$readPath = $key -replace '^HKEY_LOCAL_MACHINE', 'HKLM:' -replace '^HKEY_CURRENT_USER', 'HKCU:'
|
||
$registryPaths[$key] = Get-RegistryValuesByNamePattern -Path $readPath `
|
||
-NamePattern '(?i)^(install(path|dir)?|config(path|dir)?|data(path|dir)?|app(path|dir)?|exe(path)?|program(path|dir)?)$'
|
||
}
|
||
|
||
$installLocations = @($softwareMatches | ForEach-Object { $_['install_location'] } | Where-Object { $_ -ne 'unknown' })
|
||
|
||
return [ordered]@{
|
||
software = @($softwareMatches)
|
||
services = @($serviceMatches)
|
||
config_directories = @($directories)
|
||
registry_keys = @($registryKeys)
|
||
registry_paths = $registryPaths
|
||
install_locations = @($installLocations)
|
||
note = 'Directory and registry key names plus path-like values only; no file contents and no arbitrary registry values.'
|
||
}
|
||
}
|
||
|
||
function Get-NetworkSnapshot {
|
||
$configs = @(Get-CimInstance -ClassName Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
$addresses = @($_.IPAddress | Where-Object { $_ })
|
||
[ordered]@{
|
||
description = Get-SafeValue $_.Description
|
||
mac_address = Get-SafeValue $_.MACAddress
|
||
dhcp_enabled = [bool]$_.DHCPEnabled
|
||
dhcp_server = Get-SafeValue $_.DHCPServer
|
||
ipv4_addresses = @($addresses | Where-Object { $_ -match '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' })
|
||
ipv6_addresses = @($addresses | Where-Object { $_ -match ':' })
|
||
subnet_masks = @($_.IPSubnet | Where-Object { $_ })
|
||
default_gateways = @($_.DefaultIPGateway | Where-Object { $_ })
|
||
dns_servers = @($_.DNSServerSearchOrder | Where-Object { $_ })
|
||
dns_domain = Get-SafeValue $_.DNSDomain
|
||
dns_hostname = Get-SafeValue $_.DNSHostName
|
||
}
|
||
})
|
||
|
||
$routes = @(Get-CimInstance -ClassName Win32_IP4RouteTable -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
destination = Get-SafeValue $_.Destination
|
||
netmask = Get-SafeValue $_.Mask
|
||
next_hop = Get-SafeValue $_.NextHop
|
||
interface_index = Get-SafeValue $_.InterfaceIndex
|
||
metric = Get-SafeValue $_.Metric1
|
||
protocol = Get-SafeValue $_.Protocol
|
||
route_type = Get-SafeValue $_.Type
|
||
}
|
||
} | Sort-Object -Property { $_['destination'] })
|
||
|
||
$profiles = @()
|
||
$profileRoot = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\NetworkList\Profiles'
|
||
if (Test-Path -LiteralPath $profileRoot) {
|
||
$profiles = @(Get-ChildItem -LiteralPath $profileRoot -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
$props = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction SilentlyContinue
|
||
if ($null -eq $props) { return }
|
||
[ordered]@{
|
||
name = Get-SafeValue $props.ProfileName
|
||
description = Get-SafeValue $props.Description
|
||
category = Get-SafeValue $props.Category
|
||
}
|
||
})
|
||
}
|
||
|
||
return [ordered]@{
|
||
interfaces = $configs
|
||
macs = @($configs | ForEach-Object { $_['mac_address'] } | Where-Object { $_ -ne 'unknown' } | Select-Object -Unique)
|
||
ipv4_addresses = @($configs | ForEach-Object { $_['ipv4_addresses'] } | Where-Object { $_ })
|
||
default_gateways = @($configs | ForEach-Object { $_['default_gateways'] } | Where-Object { $_ } | Select-Object -Unique)
|
||
dns_servers = @($configs | ForEach-Object { $_['dns_servers'] } | Where-Object { $_ } | Select-Object -Unique)
|
||
routes = $routes
|
||
network_profiles = $profiles
|
||
profile_source_note = 'Network profile names come from the NetworkList registry hive; no network configuration command is invoked.'
|
||
proxy = Get-RegistryValuesByNamePattern -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' -NamePattern '(?i)^(ProxyEnable|ProxyServer|ProxyOverride|AutoConfigURL)$'
|
||
}
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Identity, parameters, dry-run gate
|
||
# ---------------------------------------------------------------------------
|
||
|
||
$dealerKnown = ($PSCmdlet.ParameterSetName -eq 'KnownDealer')
|
||
$dealerIdValue = 'unknown'
|
||
if ($dealerKnown) { $dealerIdValue = $DealerId }
|
||
$DealerId = $dealerIdValue
|
||
$deviceId = "BF-$DealerId"
|
||
$targetHostname = 'unknown'
|
||
if ($dealerKnown) { $targetHostname = "bf-$DealerId" }
|
||
|
||
$currentHostname = Get-SafeValue $env:COMPUTERNAME
|
||
if ($currentHostname -eq 'unknown') {
|
||
$currentHostname = Get-SafeValue (Get-ObjectValue (Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue) 'Name')
|
||
}
|
||
$hostName = $currentHostname.ToLower()
|
||
|
||
$now = Get-Date
|
||
$collectedAtUtc = $now.ToUniversalTime().ToString('o')
|
||
$collectedAtLocal = $now.ToString('o')
|
||
|
||
$baseName = "BF-$DealerId-windows-inventory"
|
||
$jsonPath = Join-Path $OutputDirectory "$baseName.json"
|
||
$markdownPath = Join-Path $OutputDirectory "$baseName.md"
|
||
|
||
$isElevated = $false
|
||
try {
|
||
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||
$isElevated = ([Security.Principal.WindowsPrincipal]$identity).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
} catch { $isElevated = $false }
|
||
|
||
$notes = [System.Collections.Generic.List[string]]::new()
|
||
if (-not $dealerKnown) {
|
||
$notes.Add('WARNING: dealer number unknown (-DealerIdUnknown). Output is BF-unknown-windows-inventory.* and must NOT be uploaded to the central inventory until the dealer number is confirmed and the script is re-run with -DealerId.')
|
||
}
|
||
if (-not $isElevated) {
|
||
$notes.Add('Not running elevated: some CIM sections may be incomplete. Nothing is changed either way.')
|
||
}
|
||
$notes.Add('Read-only run: no configuration, service, registry or network state is modified and no download is performed.')
|
||
|
||
if ($Check) {
|
||
$softwarePlan = 'collected'
|
||
if ($NoSoftware) { $softwarePlan = 'skipped (-NoSoftware)' }
|
||
Write-Output "CHECK: $baseName.ps1 read-only dry run"
|
||
Write-Output " dealer_id : $dealerIdValue (known: $dealerKnown)"
|
||
Write-Output " device_id : $deviceId"
|
||
Write-Output " hostname : $hostName"
|
||
Write-Output " target_hostname: $targetHostname"
|
||
Write-Output " output_json : $jsonPath"
|
||
Write-Output " output_md : $markdownPath"
|
||
Write-Output " software_surface: $softwarePlan"
|
||
Write-Output ' writes : none (dry run)'
|
||
exit 0
|
||
}
|
||
|
||
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Collection
|
||
# ---------------------------------------------------------------------------
|
||
|
||
$computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue
|
||
$bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction SilentlyContinue
|
||
$operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
|
||
$processor = Get-CimInstance -ClassName Win32_Processor -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
$videoController = Get-CimInstance -ClassName Win32_VideoController -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
|
||
$biosSmbiosVersion = Get-SafeValue (Get-ObjectValue $bios 'SMBIOSBIOSVersion')
|
||
$biosVersion = $biosSmbiosVersion
|
||
if ($biosVersion -eq 'unknown') { $biosVersion = Get-SafeValue (Get-ObjectValue $bios 'Version') }
|
||
|
||
$windowsRegistry = Get-RegistryValues -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -Names @(
|
||
'ProductName', 'EditionID', 'DisplayVersion', 'ReleaseId', 'CurrentBuild', 'UBR', 'InstallationType'
|
||
)
|
||
# Windows ProductId is a licence identifier and is deliberately not collected.
|
||
|
||
$hotfixes = @(Get-CimInstance -ClassName Win32_QuickFixEngineering -ErrorAction SilentlyContinue |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
hotfix_id = Get-SafeValue $_.HotFixID
|
||
description = Get-SafeValue $_.Description
|
||
installed_on = Get-SafeValue $_.InstalledOn
|
||
}
|
||
} | Sort-Object -Property { $_['hotfix_id'] })
|
||
|
||
$diskData = @(Get-CimInstance -ClassName Win32_DiskDrive -ErrorAction SilentlyContinue | ForEach-Object {
|
||
[ordered]@{
|
||
name = Get-SafeValue $_.Model
|
||
size_gb = ConvertTo-Gigabytes $_.Size
|
||
interface_type = Get-SafeValue $_.InterfaceType
|
||
media_type = Get-SafeValue $_.MediaType
|
||
serial = Get-SafeValue $_.SerialNumber
|
||
partitions = Get-SafeValue $_.Partitions
|
||
}
|
||
})
|
||
$volumeData = @(Get-CimInstance -ClassName Win32_LogicalDisk -Filter 'DriveType=3' -ErrorAction SilentlyContinue | ForEach-Object {
|
||
[ordered]@{
|
||
drive = Get-SafeValue $_.DeviceID
|
||
label = Get-SafeValue $_.VolumeName
|
||
file_system = Get-SafeValue $_.FileSystem
|
||
size_gb = ConvertTo-Gigabytes $_.Size
|
||
free_gb = ConvertTo-Gigabytes $_.FreeSpace
|
||
}
|
||
})
|
||
$userProfiles = @(Get-CimInstance -ClassName Win32_UserProfile -ErrorAction SilentlyContinue |
|
||
Where-Object { -not $_.Special -and $_.LocalPath } |
|
||
ForEach-Object {
|
||
[ordered]@{
|
||
path = Get-SafeValue $_.LocalPath
|
||
loaded = [bool]$_.Loaded
|
||
last_use_time = ConvertTo-IsoUtc $_.LastUseTime
|
||
}
|
||
})
|
||
|
||
$installedSoftware = @()
|
||
$serviceSnapshot = $null
|
||
$serviceRows = @()
|
||
$taskSnapshot = $null
|
||
$driverSnapshot = $null
|
||
if (-not $NoSoftware) {
|
||
$installedSoftware = @(Get-InstalledSoftware)
|
||
$serviceSnapshot = Get-ServiceSnapshot
|
||
$serviceRows = @($serviceSnapshot['running']) + @($serviceSnapshot['relevant_to_migration'])
|
||
$taskSnapshot = Get-ScheduledTaskSnapshot
|
||
$driverSnapshot = Get-DriverSnapshot
|
||
}
|
||
|
||
$megRoots = @(Get-ProgramFilesRoots)
|
||
|
||
$inventory = [ordered]@{
|
||
schema_version = 1
|
||
report_type = 'windows-pre-migration-inventory'
|
||
device_id = $deviceId
|
||
hostname = $hostName
|
||
current_hostname = $currentHostname
|
||
target_hostname = $targetHostname
|
||
dealer_id = $dealerIdValue
|
||
dealer_id_known = [bool]$dealerKnown
|
||
collected_at = $collectedAtLocal
|
||
collected_at_utc = $collectedAtUtc
|
||
read_only = $true
|
||
collection_scope = [ordered]@{
|
||
read_only = $true
|
||
elevated = [bool]$isElevated
|
||
software_surface_included = (-not $NoSoftware)
|
||
executed_external_tools = @()
|
||
excluded = @(
|
||
'credentials', 'passwords', 'private keys', 'tokens', 'file contents',
|
||
'browser profiles', 'event logs', 'DPAPI blobs', 'Windows ProductId',
|
||
'VPN tunnel/profile file contents', 'RustDesk id_ed25519 private key'
|
||
)
|
||
}
|
||
notes = @($notes)
|
||
manufacturer = Get-SafeValue (Get-ObjectValue $computerSystem 'Manufacturer')
|
||
model = Get-SafeValue (Get-ObjectValue $computerSystem 'Model')
|
||
serial = Get-SafeValue (Get-ObjectValue $bios 'SerialNumber')
|
||
bios = [ordered]@{
|
||
vendor = Get-SafeValue (Get-ObjectValue $bios 'Manufacturer')
|
||
version = $biosVersion
|
||
smbios_version = $biosSmbiosVersion
|
||
date = Get-SafeValue (Get-ObjectValue $bios 'ReleaseDate')
|
||
}
|
||
cpu = [ordered]@{
|
||
model = Get-SafeValue (Get-ObjectValue $processor 'Name')
|
||
cores = Get-SafeValue (Get-ObjectValue $processor 'NumberOfCores')
|
||
logical_processors = Get-SafeValue (Get-ObjectValue $processor 'NumberOfLogicalProcessors')
|
||
arch = Get-SafeValue (Get-ObjectValue $operatingSystem 'OSArchitecture')
|
||
address_width = Get-SafeValue (Get-ObjectValue $processor 'AddressWidth')
|
||
}
|
||
memory_mb = ConvertTo-Megabytes (Get-ObjectValue $computerSystem 'TotalPhysicalMemory')
|
||
disks = $diskData
|
||
volumes = $volumeData
|
||
gpu = [ordered]@{
|
||
model = Get-SafeValue (Get-ObjectValue $videoController 'Name')
|
||
driver_version = Get-SafeValue (Get-ObjectValue $videoController 'DriverVersion')
|
||
video_processor = Get-SafeValue (Get-ObjectValue $videoController 'VideoProcessor')
|
||
adapter_ram_mb = ConvertTo-Megabytes (Get-ObjectValue $videoController 'AdapterRAM')
|
||
}
|
||
os = [ordered]@{
|
||
distro = Get-SafeValue (Get-ObjectValue $operatingSystem 'Caption')
|
||
version = Get-SafeValue (Get-ObjectValue $operatingSystem 'Version')
|
||
build = Get-SafeValue (Get-ObjectValue $operatingSystem 'BuildNumber')
|
||
}
|
||
windows = [ordered]@{
|
||
product_name = $windowsRegistry['ProductName']
|
||
edition_id = $windowsRegistry['EditionID']
|
||
display_version = $windowsRegistry['DisplayVersion']
|
||
release_id = $windowsRegistry['ReleaseId']
|
||
current_build = $windowsRegistry['CurrentBuild']
|
||
ubr = $windowsRegistry['UBR']
|
||
installation_type = $windowsRegistry['InstallationType']
|
||
caption = Get-SafeValue (Get-ObjectValue $operatingSystem 'Caption')
|
||
version = Get-SafeValue (Get-ObjectValue $operatingSystem 'Version')
|
||
build = Get-SafeValue (Get-ObjectValue $operatingSystem 'BuildNumber')
|
||
architecture = Get-SafeValue (Get-ObjectValue $operatingSystem 'OSArchitecture')
|
||
install_date = ConvertTo-IsoUtc (Get-ObjectValue $operatingSystem 'InstallDate')
|
||
last_boot = ConvertTo-IsoUtc (Get-ObjectValue $operatingSystem 'LastBootUpTime')
|
||
product_id_collected = $false
|
||
hotfixes = $hotfixes
|
||
}
|
||
network = Get-NetworkSnapshot
|
||
serial_ports = Get-ComPortSnapshot
|
||
usb = Get-UsbSnapshot
|
||
drivers = $driverSnapshot
|
||
installed_software = $installedSoftware
|
||
services = $serviceSnapshot
|
||
scheduled_tasks = $taskSnapshot
|
||
docker = Get-DockerSnapshot
|
||
vpn = Get-VpnSnapshot -Software $installedSoftware -Services $serviceRows
|
||
remote_access = Get-RemoteAccessSnapshot -Services $serviceRows
|
||
meg = Get-MegSnapshot -Software $installedSoftware -Services $serviceRows -Roots $megRoots
|
||
user_profiles = $userProfiles
|
||
}
|
||
|
||
$inventory | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $jsonPath -Encoding utf8
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Markdown report
|
||
# ---------------------------------------------------------------------------
|
||
|
||
$lines = [System.Collections.Generic.List[string]]::new()
|
||
$lines.Add("# Windows Ön Geçiş Envanteri — $deviceId")
|
||
$lines.Add('')
|
||
$lines.Add("- Toplama zamanı (UTC): $collectedAtUtc")
|
||
$lines.Add("- Mevcut hostname: $currentHostname")
|
||
$lines.Add("- Hedef hostname (geçiş sonrası): $targetHostname")
|
||
$lines.Add("- Bayi numarası biliniyor mu: $dealerKnown")
|
||
$lines.Add("- Kapsam: yalnız okuma; kimlik bilgisi, parola, private key, dosya içeriği ve olay kaydı toplanmaz.")
|
||
$lines.Add('')
|
||
if ($notes.Count -gt 0) {
|
||
$lines.Add('## Uyarılar')
|
||
foreach ($note in $notes) { $lines.Add("- $note") }
|
||
$lines.Add('')
|
||
}
|
||
$lines.Add('## Kimlik ve donanım')
|
||
$lines.Add('')
|
||
$lines.Add('| Alan | Değer |')
|
||
$lines.Add('|---|---|')
|
||
foreach ($item in @(
|
||
@{ Name = 'Device ID'; Value = $inventory.device_id },
|
||
@{ Name = 'Mevcut hostname'; Value = $inventory.current_hostname },
|
||
@{ Name = 'Hedef hostname'; Value = $inventory.target_hostname },
|
||
@{ Name = 'Üretici'; Value = $inventory.manufacturer },
|
||
@{ Name = 'Model'; Value = $inventory.model },
|
||
@{ Name = 'Seri no'; Value = $inventory.serial },
|
||
@{ Name = 'BIOS'; Value = "$($inventory.bios.version)" },
|
||
@{ Name = 'CPU'; Value = "$($inventory.cpu.model)" },
|
||
@{ Name = 'Çekirdek'; Value = "$($inventory.cpu.cores)/$($inventory.cpu.logical_processors)" },
|
||
@{ Name = 'Mimari'; Value = $inventory.cpu.arch },
|
||
@{ Name = 'RAM (MB)'; Value = $inventory.memory_mb },
|
||
@{ Name = 'GPU'; Value = $inventory.gpu.model }
|
||
)) { $lines.Add("| $($item['Name']) | $(ConvertTo-MarkdownCell $item['Value']) |") }
|
||
$lines.Add('')
|
||
$lines.Add('## Windows sürümü')
|
||
$lines.Add('')
|
||
$lines.Add('| Alan | Değer |')
|
||
$lines.Add('|---|---|')
|
||
foreach ($item in @(
|
||
@{ Name = 'Ürün'; Value = $inventory.windows.product_name },
|
||
@{ Name = 'Sürüm (DisplayVersion)'; Value = $inventory.windows.display_version },
|
||
@{ Name = 'ReleaseId'; Value = $inventory.windows.release_id },
|
||
@{ Name = 'Build'; Value = "$($inventory.windows.current_build).$($inventory.windows.ubr)" },
|
||
@{ Name = 'Kurulum tipi'; Value = $inventory.windows.installation_type },
|
||
@{ Name = 'Kurulum tarihi'; Value = $inventory.windows.install_date },
|
||
@{ Name = 'Son boot'; Value = $inventory.windows.last_boot },
|
||
@{ Name = 'Hotfix sayısı'; Value = $inventory.windows.hotfixes.Count }
|
||
)) { $lines.Add("| $($item['Name']) | $(ConvertTo-MarkdownCell $item['Value']) |") }
|
||
$lines.Add('')
|
||
$lines.Add('## Diskler ve birimler')
|
||
$lines.Add('')
|
||
foreach ($disk in $inventory.disks) {
|
||
$lines.Add("- $($disk['name']): $($disk['size_gb']) GB ($($disk['interface_type']), $($disk['media_type'])); seri: $($disk['serial'])")
|
||
}
|
||
foreach ($volume in $inventory.volumes) {
|
||
$lines.Add("- Birim $($volume['drive']) ($($volume['label'])): $($volume['free_gb'])/$($volume['size_gb']) GB boş, $($volume['file_system'])")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Ağ')
|
||
$lines.Add('')
|
||
$lines.Add('| Bağdaştırıcı | MAC | DHCP | IPv4 | Gateway | DNS |')
|
||
$lines.Add('|---|---|---|---|---|---|')
|
||
foreach ($adapter in $inventory.network.interfaces) {
|
||
$lines.Add("| $(ConvertTo-MarkdownCell $adapter['description']) | $($adapter['mac_address']) | $($adapter['dhcp_enabled']) | $($adapter['ipv4_addresses'] -join ', ') | $($adapter['default_gateways'] -join ', ') | $($adapter['dns_servers'] -join ', ') |")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add("- Varsayılan gateway'ler: $($inventory.network.default_gateways -join ', ')")
|
||
$lines.Add("- DNS sunucuları: $($inventory.network.dns_servers -join ', ')")
|
||
$lines.Add('')
|
||
$lines.Add('### Yönlendirme tablosu (IPv4)')
|
||
$lines.Add('')
|
||
$lines.Add('| Hedef | Mask | Next hop | Metric |')
|
||
$lines.Add('|---|---|---|---|')
|
||
foreach ($route in $inventory.network.routes) {
|
||
$lines.Add("| $($route['destination']) | $($route['netmask']) | $($route['next_hop']) | $($route['metric']) |")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('### Ağ profilleri, proxy')
|
||
$lines.Add('')
|
||
foreach ($profile in $inventory.network.network_profiles) {
|
||
$lines.Add("- Profil: $($profile['name']) ($($profile['description']))")
|
||
}
|
||
foreach ($entry in $inventory.network.proxy.GetEnumerator()) {
|
||
$lines.Add("- Proxy $($entry.Key): $($entry.Value)")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Seri / COM portları')
|
||
$lines.Add('')
|
||
foreach ($port in $inventory.serial_ports.cim_serial_ports) {
|
||
$lines.Add("- $($port['device_id']): $($port['name']) ($($port['description']))")
|
||
}
|
||
foreach ($port in $inventory.serial_ports.pnp_com_devices) {
|
||
$lines.Add("- PnP: $($port['name']) — $($port['device_id']) [$($port['status'])]")
|
||
}
|
||
foreach ($entry in $inventory.serial_ports.registry_devicemap.GetEnumerator()) {
|
||
$lines.Add("- SERIALCOMM $($entry.Key) = $($entry.Value)")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## USB ve donanım kimlikleri')
|
||
$lines.Add('')
|
||
foreach ($device in $inventory.usb.devices) {
|
||
$lines.Add("- $($device['name']) — $($device['hardware_id']) [$($device['status'])]")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add("- USB denetleyici sayısı: $($inventory.usb.controllers.Count)")
|
||
$lines.Add('')
|
||
$lines.Add('## Docker')
|
||
$lines.Add('')
|
||
$lines.Add("- Kurulu: $($inventory.docker.present)")
|
||
$lines.Add("- CLI: $($inventory.docker.docker_cli_path) (sürüm: $($inventory.docker.docker_cli_file_version))")
|
||
$lines.Add("- Docker Desktop: $($inventory.docker.desktop_app_path)")
|
||
if ($null -ne $inventory.docker.service) {
|
||
$lines.Add("- Servis: $($inventory.docker.service['name']) [$($inventory.docker.service['state'])]")
|
||
}
|
||
$lines.Add("- WSL dağıtımları: $($inventory.docker.wsl_distributions -join ', ')")
|
||
$lines.Add('')
|
||
$lines.Add('## VPN istemcileri')
|
||
$lines.Add('')
|
||
$wireguardNames = @($inventory.vpn.wireguard.registry_tunnels) + @(Get-NamesOf $inventory.vpn.wireguard.tunnel_config_files)
|
||
$openVpnNames = @(Get-NamesOf $inventory.vpn.openvpn.profile_files)
|
||
$lines.Add("- WireGuard kurulu: $($inventory.vpn.wireguard.present); tüneller: $($wireguardNames -join ', ')")
|
||
$lines.Add("- OpenVPN kurulu: $($inventory.vpn.openvpn.present); profiller: $($openVpnNames -join ', ')")
|
||
foreach ($app in $inventory.vpn.installed_vpn_software) {
|
||
$lines.Add("- VPN yazılımı: $($app['name']) $($app['version'])")
|
||
}
|
||
foreach ($svc in $inventory.vpn.vpn_services) {
|
||
$lines.Add("- VPN servisi: $($svc['name']) [$($svc['state'])]")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Uzak erişim kimlikleri')
|
||
$lines.Add('')
|
||
$lines.Add("- RustDesk ID: $($inventory.remote_access.rustdesk_id)")
|
||
$lines.Add("- AnyDesk ID: $($inventory.remote_access.anydesk_id)")
|
||
foreach ($svc in $inventory.remote_access.remote_access_services) {
|
||
$lines.Add("- Uzak erişim servisi: $($svc['name']) [$($svc['state'])]")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## MEG (uygulama / servis / config yolları)')
|
||
$lines.Add('')
|
||
foreach ($app in $inventory.meg.software) {
|
||
$lines.Add("- Uygulama: $($app['name']) $($app['version']) — $($app['install_location'])")
|
||
}
|
||
foreach ($svc in $inventory.meg.services) {
|
||
$lines.Add("- Servis: $($svc['name']) [$($svc['state'])]")
|
||
}
|
||
foreach ($dir in $inventory.meg.config_directories) {
|
||
$lines.Add("- Dizin: $($dir['path']) (son yazma: $($dir['last_write_utc']))")
|
||
}
|
||
foreach ($key in $inventory.meg.registry_keys) {
|
||
$lines.Add("- Registry anahtarı: $key")
|
||
}
|
||
$lines.Add('')
|
||
if ($NoSoftware) {
|
||
$lines.Add('## Yazılım yüzeyi')
|
||
$lines.Add('')
|
||
$lines.Add('Yazılım envanteri (yüklü yazılım, servisler, zamanlanmış görevler, sürücüler) -NoSoftware nedeniyle alınmadı; ilgili JSON alanları boş bırakıldı.')
|
||
} else {
|
||
$lines.Add('## Yüklü yazılım')
|
||
$lines.Add('')
|
||
foreach ($software in $inventory.installed_software) {
|
||
$lines.Add("- $($software['name']) — $($software['version']) ($($software['publisher']))")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Servisler (çalışan, göçle ilgili, auto-start ama durmuş)')
|
||
$lines.Add('')
|
||
foreach ($svc in $inventory.services.running) {
|
||
$lines.Add("- [çalışıyor] $($svc['name']) — $($svc['display_name'])")
|
||
}
|
||
foreach ($svc in $inventory.services.auto_start_but_stopped) {
|
||
$lines.Add("- [durmuş, auto] $($svc['name']) — $($svc['display_name'])")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Zamanlanmış görevler (Microsoft dışı)')
|
||
$lines.Add('')
|
||
if ($inventory.scheduled_tasks.available) {
|
||
foreach ($task in $inventory.scheduled_tasks.items) {
|
||
$lines.Add("- $($task['task_path'])$($task['task_name']) [$($task['state'])]")
|
||
}
|
||
} else {
|
||
$lines.Add($inventory.scheduled_tasks.note)
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add("## Sürücüler ($($inventory.drivers.total) kayıt)")
|
||
$lines.Add('')
|
||
foreach ($driver in $inventory.drivers.items) {
|
||
$lines.Add("- $($driver['device_name']) — $($driver['driver_version']) ($($driver['driver_provider']))")
|
||
}
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('## Kullanıcı profilleri')
|
||
$lines.Add('')
|
||
foreach ($profile in $inventory.user_profiles) {
|
||
$lines.Add("- $($profile['path']) (son kullanım: $($profile['last_use_time']))")
|
||
}
|
||
$lines.Add('')
|
||
$lines.Add('---')
|
||
$lines.Add('')
|
||
$lines.Add('Bu rapor salt-okunur bir taramadır; hiçbir ayar, servis, ağ veya registry durumu değiştirilmedi. Veri aktarımı için BF-WindowsDataExport.ps1 kullanılır.')
|
||
$lines | Set-Content -LiteralPath $markdownPath -Encoding utf8
|
||
|
||
Write-Output "Wrote $jsonPath and $markdownPath"
|