Report mapped drives from both Windows sources, worse status wins

Windows exposes mapped drives twice and the two views disagree. Captured on one
box at one moment, same two mappings: Get-SmbMapping reported OK for both while
net use reported Disconnected and Reconnecting. Collecting only Get-SmbMapping -
the nicer, more structured one - would have reported a healthy Q: on a machine
whose Q: was down, which is the exact failure this telemetry exists to catch.

So collect both and keep them apart. The probe emits netuse and smbmapping as
separate keys and does not reconcile them; the disagreement is the signal and
normalising it in the agent would discard it before anyone could see it.

The server merges by drive letter and shows the WORSE of the two. A drive that
either source calls broken is broken for the operator, and a drive neither
source gave a status for renders as Unknown rather than blank - an unannotated
row reads as healthy, so a failed check would present as a working drive.
Drives seen by only one source are kept, not dropped: Get-SmbMapping is absent
on some builds and session types, and requiring agreement would silently hide
every mapped drive on those machines.

No active reachability testing. Windows already knows the status, so touching
each share would be wasted work and would reintroduce the hang risk the bounded
probe exists to avoid.

The probe stays unelevated in the operator's session. UAC token separation
gives an elevated context a different mapped-drive table, often empty, so
elevating this later would quietly empty the card.

Adds a commented-out remap_drive seam in the command catalog. Not implemented,
but placed so the next person finds the user-session and elevation constraints
before writing it rather than after.

The merge logic has no test coverage yet; the docstring names the three
behaviours that must not be broken.
This commit is contained in:
benjamin.baker
2026-09-05 05:47:31 -04:00
parent 511ded1ba9
commit aa901c4111
4 changed files with 711 additions and 55 deletions

View File

@@ -672,6 +672,125 @@ def panel_listeners(asset_id: int):
return success_response(rows)
def _drive_letter(value):
"""Normalise a drive to its bare letter so the two sources can be paired.
net use reports 'Z:' and Get-SmbMapping reports 'Z:' or occasionally a
trailing separator, so compare on the letter alone.
"""
text = (value or '').strip().rstrip('\\/')
return text.upper() or None
# Worst-first. The panel shows the worse of the two sources for a drive, so a
# share that one source calls OK and the other calls Unavailable is reported as
# Unavailable: a drive that is broken in either view is broken for the operator.
_DRIVE_STATUS_RANK = {
'unavailable': 0,
'disconnected': 0,
'paused': 1,
'reconnecting': 2,
'ok': 3,
'connected': 3,
}
def _worse_drive_status(left, right):
"""Return whichever status is worse, preferring a known one over None.
An unrecognised status ranks as Reconnecting, deliberately below OK: a
status we cannot interpret must not be assumed healthy.
"""
candidates = [s for s in (left, right) if s]
if not candidates:
return None
unknown_rank = _DRIVE_STATUS_RANK['reconnecting']
return min(
candidates,
key=lambda s: _DRIVE_STATUS_RANK.get(str(s).strip().lower(), unknown_rank)
)
def _mapped_drives(context):
"""Merge the two mapped-drive views into one row per drive letter.
Windows exposes mapped drives through Win32_NetworkConnection (the 'net use'
table) and through Get-SmbMapping, and they do not always agree - a share can
be Unavailable in one and OK or absent in the other. The probe collects both
without reconciling them, so the disagreement survives to here.
A drive present in only one source is still a drive: it is reported with the
status that source gave it, rather than dropped for lack of a second opinion.
WARNING - this function is not covered by tests. Three behaviours here are
load-bearing and easy to break while "simplifying":
1. Worse-status-wins. Never report the healthier of the two sources. A
drive that either source calls Unavailable IS unavailable to the
operator, and rendering it OK sends someone to a share that is down.
2. Single-source drives are kept, not dropped. Get-SmbMapping is absent on
some builds and session types; requiring agreement would silently hide
every mapped drive on those machines.
3. Unknown statuses rank as Reconnecting, not as OK. An unrecognised
string must never be optimistically treated as healthy.
Changing any of these makes a broken drive look fine, which is a silent
failure - the panel stays green and nobody is told. If you touch this,
add the tests first.
"""
drives = {}
netuse = context.get('netuse')
if isinstance(netuse, list):
for entry in netuse:
if not isinstance(entry, dict):
continue
letter = _drive_letter(entry.get('localname') or entry.get('local'))
if not letter:
continue
drives[letter] = {
'remote': entry.get('remotename') or entry.get('remote') or '',
'status': entry.get('status'),
}
smbmapping = context.get('smbmapping')
if isinstance(smbmapping, list):
for entry in smbmapping:
if not isinstance(entry, dict):
continue
letter = _drive_letter(entry.get('localpath'))
if not letter:
continue
existing = drives.get(letter)
if existing:
existing['status'] = _worse_drive_status(
existing.get('status'), entry.get('status'))
existing['remote'] = existing['remote'] or entry.get('remotepath') or ''
else:
drives[letter] = {
'remote': entry.get('remotepath') or '',
'status': entry.get('status'),
}
rows = []
for letter in sorted(drives):
drive = drives[letter]
status = (drive.get('status') or '').strip()
remote = (drive.get('remote') or '').strip()
label = '{} {}'.format(letter, remote).strip()
# A drive neither source gave a status for is Unknown, never blank. An
# unannotated row reads as healthy, so a failed check would render as a
# working drive - the one outcome this card must never produce.
if not status:
status = 'Unknown'
# Only the problem states are annotated. Tagging every healthy drive
# with "(OK)" would bury the one that is not.
if status.lower() not in ('ok', 'connected'):
label = '{} ({})'.format(label, status)
rows.append(label)
return rows
@endpoints_bp.route('/asset/<int:asset_id>/usercontext', methods=['GET'])
@require_permission('endpoints.view')
def panel_usercontext(asset_id: int):
@@ -681,6 +800,9 @@ def panel_usercontext(asset_id: int):
session 0, so they are collected by a probe run in the logged-in user's
session. Nothing is reported when no one is signed in, which is a valid
state and not a failure.
Mapped drives arrive from two sources that can disagree; the worse of the
two statuses is shown so a half-broken share is never rendered as healthy.
"""
state = EndpointState.query.filter_by(assetid=asset_id).first()
if not state:
@@ -690,21 +812,14 @@ def panel_usercontext(asset_id: int):
if not isinstance(context, dict) or not context:
return success_response({'fields': []})
netuse = context.get('netuse')
drives = []
if isinstance(netuse, list):
for entry in netuse:
if isinstance(entry, dict):
local = entry.get('localname') or entry.get('local') or ''
remote = entry.get('remotename') or entry.get('remote') or ''
drives.append('{} {}'.format(local, remote).strip())
drives = _mapped_drives(context)
listeners = context.get('userlisteners')
listenercount = len(listeners) if isinstance(listeners, list) else None
fields = [
{'label': 'Probed user', 'value': context.get('username')},
{'label': 'Collected', 'value': context.get('timestamp')},
{'label': 'Collected', 'value': context.get('collectedat')},
{'label': 'Idle', 'value': _format_uptime(context.get('idleseconds'))},
{'label': 'Mapped drives',
'value': ', '.join(drives) if drives else None, 'mono': True},