Diagnose a share that prompts, when the same UNC path works

Shopfloor PCs intermittently lose S: and ask for credentials while navigating
the identical UNC path in Explorer works fine. Four different causes produce
that same appearance - a persistent reconnect that raced the network, a stored
credential whose target string does not cover the name form in use, two
identities contending for one server, or a password that rotated - and each
needs a different fix. From the desk they are indistinguishable.

Test-ShareAccess.ps1 collects what separates them: reachability of both name
forms, the mappings and the account each remembers, the exact credential target
strings, Kerberos tickets, live SMB sessions and the identities holding them,
and the SMBClient/Connectivity log. Then it names the causes the evidence
supports rather than leaving a wall of output to interpret.

Read-only by construction: it maps nothing, deletes nothing, stores no
credential, and prints usernames and a password-present flag but never a
password, because its log lands where techs read it. Safe on a bay while
somebody is working.

It reports which identity it is running as, because mappings and SMB sessions
are per logon session: run as the user to explain S:, as SYSTEM to explain W:
and the backup scripts. A drive missing in one is genuinely absent in the
other, and mistaking that for a fault is easy.

Also derives the marker backup's share root from the script's own location
rather than a hardcoded UNC. GE-Enforce mounts the share as W: with SFLD
credentials, and reaching the same server by a different name than the mount
used costs a second authentication - or fails outright with 1219 - for reasons
the error text never mentions. The script's own path is the one handle
guaranteed to name the connection that already exists.
This commit is contained in:
cproudlock
2026-08-25 13:44:05 -04:00
parent 18168b54f8
commit bd131f6da9
2 changed files with 294 additions and 1 deletions

View File

@@ -0,0 +1,249 @@
# Test-ShareAccess.ps1 - why can this PC not reach the share?
#
# READ-ONLY. It maps nothing, deletes nothing, stores no credential and prints
# no password. Safe to run on a production bay while somebody is working.
#
# THE PROBLEM IT EXISTS FOR
#
# Shopfloor PCs intermittently lose S: - it prompts for credentials - while
# navigating the identical UNC path in Explorer works fine. That combination has
# several possible causes which look identical from the desk, and the fix
# differs for each:
#
# 1. Persistent reconnect raced the network. The mapping is restored early in
# logon, before wifi/802.1X is up, so it comes back dead. A manual UNC
# visit minutes later works because by then the server is reachable.
# 2. The stored credential does not cover the name form in use. Credential
# Manager matches by STRING: an entry for "server" does not serve
# "server.domain.com". One path finds the account, the other does not.
# 3. Two identities for one server. Windows allows ONE SMB session per server
# per logon session, so if the drive wants a shared account and something
# already opened that server as the logged-on user (or by a second name
# form), the loser gets a credential prompt rather than a clear error.
# 4. Genuinely stale password, after a rotation.
#
# This collects the evidence that separates them, and says which it looks like.
#
# CONTEXT MATTERS: drive mappings and SMB sessions are PER LOGON SESSION. Run it
# as the logged-on user to diagnose S:. Run it as SYSTEM (psexec -s, or a
# GE-Enforce manifest entry) to diagnose W: and the backup scripts. The two see
# different worlds, and a mapping visible in one is genuinely absent in the
# other - that is not a bug.
#
# Log: C:\Logs\Shopfloor\share-access-<yyyyMMdd-HHmmss>.log
param(
# Server to test. Both the short and fully-qualified forms are exercised,
# because a mismatch between them is one of the causes.
[string]$Server = 'tsgwp00525.wjs.geaerospace.net',
# Share to probe on that server.
[string]$Share = 'shared',
# Drive letters to report on. S: is the user's shopfloor mapping, W: is the
# one GE-Enforce makes as SYSTEM for the duration of its cycle.
[string[]]$Drives = @('S:', 'W:')
)
$ErrorActionPreference = 'Continue'
$logDir = 'C:\Logs\Shopfloor'
if (-not (Test-Path $logDir)) {
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
}
$logFile = Join-Path $logDir ('share-access-{0}.log' -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
function Say {
param([string]$Text = '')
Write-Host $Text
Add-Content -Path $logFile -Value $Text -ErrorAction SilentlyContinue
}
function Section {
param([string]$Title)
Say ''
Say ('=== {0} ' -f $Title).PadRight(70, '=')
}
$short = ($Server -split '\.')[0]
$forms = @($Server, $short) | Select-Object -Unique
$findings = New-Object System.Collections.Generic.List[string]
Say ('Test-ShareAccess {0}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))
Say ('Log: {0}' -f $logFile)
# ---------------------------------------------------------------------------
Section 'Who is running this'
# Everything below is scoped to this identity and this logon session. A drive
# absent here may be perfectly healthy in another session.
$me = [Security.Principal.WindowsIdentity]::GetCurrent()
$isSystem = $me.IsSystem
Say ('User : {0}{1}' -f $me.Name, $(if ($isSystem) { ' (SYSTEM - sees W:, NOT the user''s S:)' } else { '' }))
Say ('Computer : {0}' -f $env:COMPUTERNAME)
if (-not $isSystem) {
Say 'Note : run this again as SYSTEM (psexec -s) to see what GE-Enforce sees.'
}
# ---------------------------------------------------------------------------
Section 'Name resolution and reachability'
# A dead mapping with a healthy UNC path is often just timing: if the server is
# unreachable at logon the persistent reconnect fails, and by the time anyone
# looks the network is fine.
foreach ($form in $forms) {
$ip = ''
try {
$ip = ([Net.Dns]::GetHostAddresses($form) |
Where-Object { $_.AddressFamily -eq 'InterNetwork' } |
Select-Object -First 1).IPAddressToString
} catch { $ip = 'DNS FAILED' }
$port = 'no'
try {
$client = New-Object Net.Sockets.TcpClient
$async = $client.BeginConnect($form, 445, $null, $null)
if ($async.AsyncWaitHandle.WaitOne(3000) -and $client.Connected) { $port = 'yes' }
$client.Close()
} catch { }
Say ('{0,-45} -> {1,-16} SMB/445 reachable: {2}' -f $form, $ip, $port)
if ($port -ne 'yes') {
$findings.Add("Cannot reach $form on 445 right now. If the mapping failed at logon for the same reason, that is cause 1 - a reconnect that raced the network.")
}
}
# ---------------------------------------------------------------------------
Section 'Drive mappings in THIS session'
# The 'user' column is the point: a drive remembering an account other than the
# one running this is what makes a credential prompt possible at all.
$netuse = & net use 2>&1 | Out-String
Say $netuse.TrimEnd()
foreach ($drive in $Drives) {
$line = ($netuse -split "`r?`n" | Where-Object { $_ -match [regex]::Escape($drive) } | Select-Object -First 1)
if (-not $line) {
Say ('{0} not present in this session.' -f $drive)
} elseif ($line -match 'Unavailable|Disconnected') {
Say ('{0} present but NOT connected.' -f $drive)
$findings.Add("$drive exists but is not connected - the classic dead persistent reconnect (cause 1).")
}
}
# ---------------------------------------------------------------------------
Section 'Stored credentials'
# cmdkey matches by STRING, so the exact Target text decides whether a mapping
# finds its account. This prints targets and usernames only - never a password,
# and cmdkey cannot reveal one anyway.
$cmdkey = & cmdkey /list 2>&1 | Out-String
$targets = [regex]::Matches($cmdkey, 'Target:\s*(\S+)') | ForEach-Object { $_.Groups[1].Value }
if ($targets) {
Say 'Targets stored for this user:'
foreach ($t in $targets) { Say (' {0}' -f $t) }
} else {
Say 'No stored credentials for this user.'
}
foreach ($form in $forms) {
$covered = @($targets | Where-Object {
$_ -match [regex]::Escape($form) -or
($_ -match '^\*' -and $form -like ($_ -replace '^Domain:target=', ''))
})
if ($covered.Count -eq 0) {
Say ('NO stored credential matches "{0}"' -f $form)
$findings.Add("No credential entry matches '$form'. If the mapping uses that form, it has no account to reconnect with (cause 2). Add an entry for every name form in use, or a wildcard.")
} else {
Say ('Credential covers "{0}": {1}' -f $form, ($covered -join ', '))
}
}
# ---------------------------------------------------------------------------
Section 'Kerberos tickets'
# A cifs/ ticket for the server means this identity can reach it on its own.
# On a shopfloor bay the logon usually CANNOT, which is why a stored account is
# mandatory - and that is what makes two-identities-one-server possible.
$klist = & klist 2>&1 | Out-String
$cifs = [regex]::Matches($klist, 'cifs/[^\s@]+') | ForEach-Object { $_.Value } | Select-Object -Unique
if ($cifs) { Say ('cifs tickets: {0}' -f ($cifs -join ', ')) } else { Say 'No cifs tickets for this identity.' }
# ---------------------------------------------------------------------------
Section 'Live SMB sessions to this server'
# ONE session per server per logon session. Two name forms count as two servers
# - which is how a single share ends up wanting two identities at once.
$connections = @()
try {
$connections = @(Get-SmbConnection -ErrorAction Stop |
Where-Object { $_.ServerName -match [regex]::Escape($short) })
} catch { }
if ($connections.Count) {
foreach ($c in $connections) {
Say ('{0,-45} share {1,-14} as {2}' -f $c.ServerName, $c.ShareName, $c.UserName)
}
$identities = @($connections | Select-Object -ExpandProperty UserName -Unique)
$servers = @($connections | Select-Object -ExpandProperty ServerName -Unique)
if ($identities.Count -gt 1) {
$findings.Add("More than one identity is connected to this server in this session ($($identities -join ', ')). That is cause 3, and whichever connected first wins - the other gets a credential prompt.")
}
if ($servers.Count -gt 1) {
$findings.Add("This server is connected under more than one NAME ($($servers -join ', ')). Windows treats those as different servers, so they get separate sessions and separate credential lookups.")
}
} else {
Say 'No live SMB session to this server in this logon session.'
}
# ---------------------------------------------------------------------------
Section 'Can this identity read the share right now'
# Test-Path is deliberately not trusted on its own: a SYSTEM process denied
# access to a UNC path gets a "not found", so a permissions problem reads as a
# missing directory.
foreach ($form in $forms) {
$path = '\\{0}\{1}' -f $form, $Share
$ok = $false
try { $ok = Test-Path -LiteralPath $path -ErrorAction Stop } catch { }
Say ('{0,-55} readable: {1}' -f $path, $ok)
if (-not $ok -and $isSystem) {
Say ' (as SYSTEM a denial reports as "not found" - do not read this as a missing path)'
}
}
# ---------------------------------------------------------------------------
Section 'SFLD credential provisioning (GE-Enforce)'
# Username and presence only. The password is never printed - it is a fleet
# credential and this log is written to a directory techs read.
$sfldBase = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
if (Test-Path $sfldBase) {
foreach ($key in Get-ChildItem $sfldBase -ErrorAction SilentlyContinue) {
$props = Get-ItemProperty -Path $key.PSPath -ErrorAction SilentlyContinue
$names = @($props.PSObject.Properties.Name)
$user = if ($names -contains 'Username') { $props.Username } else { '(none)' }
$hasPassword = ($names -contains 'Password') -and $props.Password
Say ('{0,-30} user {1,-28} password present: {2}' -f $key.PSChildName, $user, [bool]$hasPassword)
}
} else {
Say 'No SFLD credentials provisioned (Azure DSC has not run, or this is not a fleet PC).'
if ($isSystem) {
$findings.Add('No SFLD credential in the registry, so GE-Enforce cannot mount the share at all this cycle.')
}
}
# ---------------------------------------------------------------------------
Section 'Recent SMB client errors'
try {
$events = Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-SMBClient/Connectivity'; StartTime = (Get-Date).AddDays(-3)
} -MaxEvents 8 -ErrorAction Stop
foreach ($e in $events) {
Say ('{0} id {1} {2}' -f $e.TimeCreated.ToString('MM-dd HH:mm'), $e.Id,
(($e.Message -split "`r?`n")[0]))
}
} catch {
Say 'No SMBClient/Connectivity events in the last 3 days (or the log is not enabled).'
}
# ---------------------------------------------------------------------------
Section 'What this looks like'
if ($findings.Count -eq 0) {
Say 'Nothing conclusive. If the drive is failing only at logon, run this again'
Say 'IMMEDIATELY after a failure - several of these signals are transient and'
Say 'a working session masks the state that caused the prompt.'
} else {
$n = 0
foreach ($f in $findings) { $n++; Say ('{0}. {1}' -f $n, $f) }
}
Say ''
Say ('Full log: {0}' -f $logFile)
exit 0