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.
250 lines
11 KiB
PowerShell
250 lines
11 KiB
PowerShell
# 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
|