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:
@@ -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},
|
||||
|
||||
@@ -102,6 +102,27 @@ COMMAND_ALLOWLIST = {
|
||||
}
|
||||
|
||||
|
||||
# Reserved seam - one-click mapped-drive remediation. NOT implemented; this is a
|
||||
# placement note so the next person adds it here rather than inventing a new
|
||||
# transport. When it lands it will re-establish a Disconnected mapping surfaced
|
||||
# by the user-context card:
|
||||
#
|
||||
# 'remap_drive': {
|
||||
# 'label': 'Reconnect mapped drive',
|
||||
# 'description': 'Re-establish a disconnected network drive mapping.',
|
||||
# 'targetkind': 'drive',
|
||||
# },
|
||||
#
|
||||
# It MUST execute in the interactive USER session. A SYSTEM-context remap creates
|
||||
# a mapping in SYSTEM's own drive table that the operator cannot see, so the card
|
||||
# would keep reporting the drive as broken while the "fix" silently succeeded
|
||||
# somewhere nobody is looking. An ELEVATED remap fails the same way, because UAC
|
||||
# token separation gives the elevated context a different drive table - the same
|
||||
# reason the probe itself runs unelevated from the tray.
|
||||
#
|
||||
# CommandExecutor.RunScriptInUserSession is the existing path for this.
|
||||
|
||||
|
||||
# Actions that demand a permission stricter than the base endpoints.command.
|
||||
# The enqueue route requires the caller to ALSO hold the mapped permission, so a
|
||||
# destructive action (a remote reboot) is a heavier grant than a routine toggle.
|
||||
|
||||
Reference in New Issue
Block a user