# 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-.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