Add Corporate Tools tab, Dell Command actions, and user-context refresh
Corporate Tools tab on PC Details, gated by endpoints.deploy and liveness. Starts already-registered scheduled tasks; the agent's ScheduledTaskPolicy allowlist remains the authority on what may actually run. Dell Command | Update split into two actions (Scan, Apply Updates) because they have very different consequences. Apply Updates carries a persistent breakglass warning in the card naming Tanium as the primary update utility, rendered between the row title and description so it is visible at all times rather than only in the confirm dialog. Destructive actions keep the typed RUN confirmation. Fix scheduledtask_run being stripped by Configuration Policy: the intersection in ALLOWED_ACTIONS_FOR dropped it on any endpoint whose effective policy was non-empty, so every Corporate Tools click failed with "not permitted by policy" regardless of the caller's role. Corporate tools are role-gated only, via POLICY_EXEMPT_ACTIONS; policy keeps authority over monitors and service/process primitives. Fix blank PC Details page: watch(detailtabs) evaluates its source during setup and read canDeploy before its const declaration, throwing a TDZ ReferenceError that aborted setup with no API request made. Mapped-drive refresh moves to the tray. SYSTEM cannot probe user-scoped drives, so the service requests a re-probe over a named event and waits for the spool collectedat stamp to advance before collecting. The probe task is registered as a ShopFloor user principal; the Users-group principal remains only as a degraded fallback since a group member cannot start it.
This commit is contained in:
@@ -494,6 +494,60 @@ def _drift_for_state(state):
|
||||
return parsed if isinstance(parsed, list) else []
|
||||
|
||||
|
||||
def _dell_updates_summary(state):
|
||||
"""Parse the cached Dell Command | Update summary off an EndpointState.
|
||||
|
||||
dellupdatesjson is written on check-in from the shopdb-dellupdates agent
|
||||
utility, which reads the log the \\ShopDB-DellCommandScan task produces.
|
||||
Null means the endpoint has never reported a scan. Tolerant of a malformed
|
||||
blob so a bad cache never breaks the panel.
|
||||
"""
|
||||
raw = getattr(state, 'dellupdatesjson', None) if state else None
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return parsed if isinstance(parsed, dict) else None
|
||||
|
||||
|
||||
def _dell_updates_value(state):
|
||||
"""Render the informational 'Pending Dell updates' status value.
|
||||
|
||||
Informational only - this row never claims an endpoint needs attention,
|
||||
because a pending optional driver is not a support problem. Three states are
|
||||
kept distinct on purpose, and all three are VISIBLE:
|
||||
|
||||
never scanned -> 'Never scanned'. Deliberately not 'No': those are
|
||||
different facts, and claiming 'No' would tell an operator
|
||||
the machine is current when nobody has ever checked. It
|
||||
is also not hidden - a missing row reads as a broken
|
||||
panel, so the unknown state says so out loud.
|
||||
scanned clean -> 'No'.
|
||||
pending -> 'Yes (n)', with the count because one optional update and
|
||||
fourteen urgent ones warrant different urgency.
|
||||
"""
|
||||
summary = _dell_updates_summary(state)
|
||||
if not summary:
|
||||
return 'Never scanned'
|
||||
status = summary.get('status')
|
||||
if status == 'error':
|
||||
# The scan log exists but could not be read. A different problem from
|
||||
# never having scanned, and worth saying so rather than implying the
|
||||
# endpoint was never checked.
|
||||
return 'Unknown (scan unreadable)'
|
||||
if status != 'ok':
|
||||
return 'Never scanned'
|
||||
try:
|
||||
count = int(summary.get('count') or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 'Unknown (scan unreadable)'
|
||||
if count <= 0:
|
||||
return 'No'
|
||||
return 'Yes ({})'.format(count)
|
||||
|
||||
|
||||
@endpoints_bp.route('/asset/<int:asset_id>/status', methods=['GET'])
|
||||
@require_permission('endpoints.view')
|
||||
def panel_status(asset_id: int):
|
||||
@@ -530,6 +584,11 @@ def panel_status(asset_id: int):
|
||||
]
|
||||
fields = [f for f in fields if f['value'] not in (None, '')]
|
||||
|
||||
# Informational only, and appended after the null-filter above because it
|
||||
# always renders - including an explicit "Never scanned", which a hidden row
|
||||
# would leave looking like a broken panel.
|
||||
fields.append(field('Pending Dell updates', _dell_updates_value(state)))
|
||||
|
||||
# Warn-only reconciler summary: monitors site policy expects to be running
|
||||
# that the last check-in did not report as running. Surfaced as a status
|
||||
# field so an operator sees the drift on the panel; nothing is auto-started.
|
||||
|
||||
@@ -136,6 +136,12 @@ _BASE_APPSETTINGS = {
|
||||
'UDC.exe': 'C:\\Program Files\\UDC\\UDC.exe',
|
||||
'agent.exe': 'C:\\ProgramData\\udc\\mtconnect_udc\\agent\\run.bat',
|
||||
},
|
||||
# Approved scheduled task paths for scheduledtask_run. Shipped empty, which
|
||||
# means the agent falls back to ScheduledTaskPolicy.DefaultApprovedTaskPaths
|
||||
# (the Corporate Tools set). Listing entries here REPLACES those defaults, so
|
||||
# a site can narrow or widen the set without an agent rebuild. Anything not
|
||||
# matched fails closed.
|
||||
'approvedtaskpaths': [],
|
||||
}
|
||||
|
||||
_README = """ShopDB endpoint agent (site build)
|
||||
@@ -224,6 +230,11 @@ param(
|
||||
# reliable way to collect them; see the registration block below.
|
||||
[string] $UserContextTaskName = "ShopDB user-context probe",
|
||||
[int] $UserContextTaskMinutes = 30,
|
||||
|
||||
# The account the probe task runs as. Must match the account whose profile
|
||||
# the agent reads the spool from (FileLoggerProvider.ShopFloorUserName), or
|
||||
# the probe writes session data the service never looks at.
|
||||
[string] $UserContextTaskUser = "ShopFloor",
|
||||
[switch] $NoUserContextTask
|
||||
)
|
||||
|
||||
@@ -385,14 +396,23 @@ if (-not $NoTray) {{
|
||||
}}
|
||||
}}
|
||||
|
||||
# 2b. Register the user-context probe as a scheduled task owned by the Users
|
||||
# GROUP. This is what makes mapped drives, user listeners and idle time
|
||||
# 2b. Register the user-context probe as a scheduled task owned by the SHOPFLOOR
|
||||
# USER. This is what makes mapped drives, user listeners and idle time
|
||||
# collectable: it is the only mechanism that starts a process in the operator's
|
||||
# own session with the operator's own token.
|
||||
#
|
||||
# RunLevel Limited is load-bearing, not a hardening nicety - the unelevated
|
||||
# token is precisely what sees the operator's mapped drives. GroupId rather
|
||||
# than UserId so the task runs as WHOEVER signs in, each in their own session.
|
||||
# token is precisely what sees the operator's mapped drives.
|
||||
#
|
||||
# UserId, NOT GroupId. A group-principal task is registered against the group
|
||||
# rather than against any one account, and a member of that group cannot invoke
|
||||
# it on demand: Start-ScheduledTask from the ShopFloor session returns access
|
||||
# denied, so the task only ever ran on its own trigger. That is exactly the bug
|
||||
# where "Refresh Status" appeared to do nothing. Registering to the specific
|
||||
# account makes the task startable by that account, which is what both the tray
|
||||
# and any manual retry depend on. The group form is kept only as a fallback for
|
||||
# a host where the account cannot be resolved - it still collects on a timer,
|
||||
# it just cannot be kicked on demand.
|
||||
$taskState = "skipped"
|
||||
if (-not $NoUserContextTask) {{
|
||||
Write-Host " registering user-context probe task ..."
|
||||
@@ -410,15 +430,26 @@ if (-not $NoUserContextTask) {{
|
||||
}}
|
||||
$taskSettings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
|
||||
# BUILTIN\\Users by SID first, then by name: the SID is locale
|
||||
# independent (on non-English Windows the group is not called "Users"),
|
||||
# but a hardened host can still reject it.
|
||||
foreach ($groupId in @("S-1-5-32-545", "BUILTIN\\Users")) {{
|
||||
# Try the machine-qualified account first, then the bare name. Interactive
|
||||
# LogonType so the task joins the existing interactive session instead of
|
||||
# creating a separate one with its own (empty) drive table.
|
||||
$taskCandidates = @(
|
||||
@{{ Kind = "user"; Id = "$env:COMPUTERNAME\\$UserContextTaskUser" }},
|
||||
@{{ Kind = "user"; Id = $UserContextTaskUser }},
|
||||
@{{ Kind = "group"; Id = "S-1-5-32-545" }},
|
||||
@{{ Kind = "group"; Id = "BUILTIN\\Users" }}
|
||||
)
|
||||
foreach ($candidate in $taskCandidates) {{
|
||||
try {{
|
||||
$taskPrincipal = New-ScheduledTaskPrincipal -GroupId $groupId -RunLevel Limited -ErrorAction Stop
|
||||
if ($candidate.Kind -eq "user") {{
|
||||
$taskPrincipal = New-ScheduledTaskPrincipal -UserId $candidate.Id -LogonType Interactive -RunLevel Limited -ErrorAction Stop
|
||||
}}
|
||||
else {{
|
||||
$taskPrincipal = New-ScheduledTaskPrincipal -GroupId $candidate.Id -RunLevel Limited -ErrorAction Stop
|
||||
}}
|
||||
Register-ScheduledTask -TaskName $UserContextTaskName -Action $taskAction -Trigger $taskTriggers -Principal $taskPrincipal -Settings $taskSettings -Force -ErrorAction Stop | Out-Null
|
||||
$taskRegistered = $true
|
||||
$taskDetail = $groupId
|
||||
$taskDetail = "$($candidate.Kind):$($candidate.Id)"
|
||||
break
|
||||
}}
|
||||
catch {{ $taskDetail = $_.Exception.Message }}
|
||||
@@ -440,12 +471,17 @@ if (-not $NoUserContextTask) {{
|
||||
$taskState = "UNVERIFIED:elevated"
|
||||
Write-Warning " user-context task runs ELEVATED; mapped drives would be collected from the wrong token."
|
||||
}}
|
||||
elseif (-not $verified.Principal.GroupId) {{
|
||||
$taskState = "UNVERIFIED:no group principal"
|
||||
Write-Warning " user-context task has no group principal; it would run as one specific account."
|
||||
elseif ($verified.Principal.GroupId) {{
|
||||
# Registered, will collect on its triggers, but cannot be started on
|
||||
# demand by the operator - so "Refresh Status" will fall back to the
|
||||
# timer. Surfaced rather than reported as ok, because the symptom
|
||||
# (stale session data that never responds to a refresh) is otherwise
|
||||
# indistinguishable from a broken probe.
|
||||
$taskState = "DEGRADED:group principal $($verified.Principal.GroupId)"
|
||||
Write-Warning " user-context task fell back to a GROUP principal; on-demand refresh will not work, only the $UserContextTaskMinutes-minute interval."
|
||||
}}
|
||||
else {{
|
||||
$taskState = "ok:$($verified.Principal.GroupId)/$($verified.Principal.RunLevel)"
|
||||
$taskState = "ok:$($verified.Principal.UserId)/$($verified.Principal.RunLevel)"
|
||||
Write-Host " user-context task registered and verified: $taskState"
|
||||
}}
|
||||
|
||||
|
||||
@@ -83,12 +83,30 @@ COMMAND_ALLOWLIST = {
|
||||
'Read-only diagnostic; the standard report carries only '
|
||||
'listening ports because established connections churn.',
|
||||
},
|
||||
'scheduledtask_run': {
|
||||
'label': 'Run scheduled task',
|
||||
'description': 'Start an already-registered Windows scheduled task on '
|
||||
'the endpoint by its task path (for example '
|
||||
'\\ReportIP). The agent does not create, modify, or '
|
||||
'define tasks - it only starts one that already exists, '
|
||||
'and only when the path clears the agent-side approved '
|
||||
'task allowlist. The task runs in whatever context its '
|
||||
'own registered definition specifies, not the agent\'s.',
|
||||
'targetkind': 'scheduledtask',
|
||||
},
|
||||
'run_script': {
|
||||
'label': 'Run script',
|
||||
'description': 'Run a ShopDB-hosted script on the endpoint. The agent '
|
||||
'fetches the script body by digest using its collector '
|
||||
'credential and runs it in the script\'s configured '
|
||||
'context (system or the logged-on user).',
|
||||
'context (system or the logged-on user). CAUTION: a '
|
||||
'script sent this way runs as SYSTEM unless its '
|
||||
'descriptor pins the user context, so this is arbitrary '
|
||||
'code execution on the endpoint by design. Grant '
|
||||
'endpoints.deploy only to the most trusted operators, '
|
||||
'and gate the UI behind an explicit confirm the way '
|
||||
'system_restart is gated (see confirmRestart in '
|
||||
'PCDetail.vue).',
|
||||
'targetkind': 'script',
|
||||
},
|
||||
'update_app': {
|
||||
@@ -102,6 +120,35 @@ COMMAND_ALLOWLIST = {
|
||||
}
|
||||
|
||||
|
||||
# Dell Command | Update: TWO utilities, deliberately two separate actions.
|
||||
# A scan reports what is available and changes nothing; an update installs and
|
||||
# can reboot. Collapsing them into one parameterized action would make the
|
||||
# destructive one reachable by editing a field, so they stay distinct and the
|
||||
# UI can gate them differently.
|
||||
#
|
||||
# Both are driven through scheduledtask_run against ShopDB-owned task
|
||||
# definitions (\ShopDB-DellCommandScan and \ShopDB-DellCommandUpdate, already in
|
||||
# ScheduledTaskPolicy's shipped defaults) rather than a bespoke verb. The task
|
||||
# definition - not the agent - pins the dcu-cli.exe path, its arguments, and its
|
||||
# output path, which is why no new agent code is needed to run them.
|
||||
#
|
||||
# OUTPUT PATH CONVENTION. A ShopDB-initiated scan MUST write its report under
|
||||
# %ProgramData%\ShopDB\EndpointAgent\ (the same SYSTEM-owned root as the agent
|
||||
# logs), NOT to Dell's default location. Corporate-managed DCU tasks write to
|
||||
# the default; keeping ShopDB's output separate is the only thing that makes
|
||||
# "did ShopDB ask for this scan, or did corporate" answerable when the results
|
||||
# are collected. The path lives in the task definition and is generated
|
||||
# server-side, so it can change without touching the agent.
|
||||
#
|
||||
# STILL TO DO - the results collector. Firing DCU is the easy half. schtasks
|
||||
# /run returns as soon as the task STARTS, so a successful scheduledtask_run
|
||||
# means "scan started", never "scan finished" - the command result cannot carry
|
||||
# findings. Reporting available updates back into ShopDB needs a collector that
|
||||
# reads the report from the path above and folds it into the telemetry payload.
|
||||
# That is a separate change from this one; do not infer from a green command
|
||||
# result that the data path exists.
|
||||
|
||||
|
||||
# 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
|
||||
@@ -126,8 +173,23 @@ COMMAND_ALLOWLIST = {
|
||||
# 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.
|
||||
# NOTE FOR THE ROLES/PERMISSIONS WORK: scheduledtask_run is deliberately mapped
|
||||
# onto the EXISTING endpoints.deploy grant rather than introducing a new one, so
|
||||
# the wire is pre-built and needs no migration. Be aware this WIDENS what
|
||||
# endpoints.deploy means: it previously covered run_script + update_app, and now
|
||||
# also covers starting approved scheduled tasks. If that bundle is too coarse,
|
||||
# the clean split is a separate endpoints.corptools permission mapped here - no
|
||||
# other code needs to change, because the enqueue route reads this table.
|
||||
#
|
||||
# Suggested operator-facing wording for the combined grant:
|
||||
# "Caution: Operator can run scheduled tasks, update applications, and affect
|
||||
# system-level changes to the endpoint."
|
||||
# That wording is accurate for scheduledtask_run and update_app. It UNDERSTATES
|
||||
# run_script, which is arbitrary SYSTEM code execution - if run_script stays in
|
||||
# this bundle, say so plainly in the role description.
|
||||
ELEVATED_ACTION_PERMISSIONS = {
|
||||
'system_restart': 'endpoints.restart',
|
||||
'scheduledtask_run': 'endpoints.deploy',
|
||||
'run_script': 'endpoints.deploy',
|
||||
'update_app': 'endpoints.deploy',
|
||||
}
|
||||
@@ -242,6 +304,24 @@ def monitor_id_from_action(action):
|
||||
return None
|
||||
|
||||
|
||||
# Actions gated by ROLE ONLY, never by per-endpoint Configuration Policy.
|
||||
#
|
||||
# These are the Corporate Tools actions. They start already-registered,
|
||||
# agent-allowlisted tasks and are not a per-machine configuration concern, so a
|
||||
# policy that never mentions them must not remove them. Policy exists to shape
|
||||
# what a given PC TYPE exposes (which monitors, which service primitives); it is
|
||||
# not the authority on whether an admin may run a corporate tool.
|
||||
#
|
||||
# Without this exemption the intersection at the end of ALLOWED_ACTIONS_FOR
|
||||
# silently drops these actions on any endpoint whose effective policy is
|
||||
# non-empty, producing "Action not permitted by policy for this endpoint" even
|
||||
# for a caller holding endpoints.deploy. The real gate for these stays
|
||||
# ELEVATED_ACTION_PERMISSIONS (role), enforced in the enqueue route.
|
||||
POLICY_EXEMPT_ACTIONS = frozenset({
|
||||
'scheduledtask_run',
|
||||
})
|
||||
|
||||
|
||||
def ALLOWED_ACTIONS_FOR(pctype=None, assetid=None):
|
||||
"""The action names an endpoint may run.
|
||||
|
||||
@@ -250,8 +330,9 @@ def ALLOWED_ACTIONS_FOR(pctype=None, assetid=None):
|
||||
result is narrowed to the actions the endpoint's effective policy permits -
|
||||
this is the allowlist-gating half of the policy design. With the feature off
|
||||
(or no assetid), it returns the full base allowlist, so nothing changes until
|
||||
a site opts in. Policy resolution is imported lazily to keep this module free
|
||||
of a hard dependency on the computers plugin.
|
||||
a site opts in. Actions in POLICY_EXEMPT_ACTIONS are never narrowed away;
|
||||
they are gated by role alone. Policy resolution is imported lazily to keep
|
||||
this module free of a hard dependency on the computers plugin.
|
||||
"""
|
||||
base = set(COMMAND_ALLOWLIST.keys())
|
||||
if assetid is None:
|
||||
@@ -274,6 +355,9 @@ def ALLOWED_ACTIONS_FOR(pctype=None, assetid=None):
|
||||
# "not permitted by policy". Expand each monitor:<id> to its kind's control
|
||||
# actions before intersecting.
|
||||
allowed = allowed | _monitor_control_actions(allowed)
|
||||
# Role-gated actions are added back unconditionally: policy narrows the
|
||||
# per-endpoint surface, but it does not own the corporate tools.
|
||||
allowed = allowed | POLICY_EXEMPT_ACTIONS
|
||||
# Never widen past the code-level allowlist: policy can only gate within it.
|
||||
return base & allowed
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Add dellupdatesjson to endpointstates.
|
||||
|
||||
Stores the pending Dell Command | Update summary reported by the
|
||||
shopdb-dellupdates agent utility. MEDIUMTEXT for consistency with the other
|
||||
snapshot columns on this table.
|
||||
|
||||
Nullable with no backfill. Null means the endpoint has never reported a Dell
|
||||
scan result, which is deliberately distinct from a stored payload whose status
|
||||
is 'ok' with an empty list (scanned, nothing pending). The status card renders
|
||||
those two states differently, so defaulting this to an empty summary would
|
||||
assert every pre-existing endpoint had been scanned and found clean.
|
||||
|
||||
Idempotent: the column-add checks the live schema first, so a re-run and a
|
||||
db.create_all() site both stay clean on strict MySQL 8.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'endpoints0016dellupdates'
|
||||
down_revision = 'endpoints0015systemtelemetry'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
TABLE_NAME = 'endpointstates'
|
||||
|
||||
COLUMN_NAME = 'dellupdatesjson'
|
||||
|
||||
|
||||
def _has_column(table_name, column_name):
|
||||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(op.get_bind())
|
||||
if not inspector.has_table(table_name):
|
||||
return False
|
||||
return any(col['name'] == column_name
|
||||
for col in inspector.get_columns(table_name))
|
||||
|
||||
|
||||
def upgrade():
|
||||
if not _has_column(TABLE_NAME, COLUMN_NAME):
|
||||
op.add_column(
|
||||
TABLE_NAME,
|
||||
sa.Column(COLUMN_NAME, sa.Text(length=16777215), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
if _has_column(TABLE_NAME, COLUMN_NAME):
|
||||
op.drop_column(TABLE_NAME, COLUMN_NAME)
|
||||
@@ -129,6 +129,16 @@ class EndpointState(db.Model):
|
||||
# already accumulating when someone is ready to interpret it.
|
||||
defenderjson = db.Column(db.Text(length=16777215), nullable=True)
|
||||
|
||||
# Pending Dell Command | Update items, produced by the shopdb-dellupdates
|
||||
# agent utility from the log the \ShopDB-DellCommandScan task writes. Only
|
||||
# the count drives the status card; the short list is stored so the panel
|
||||
# can show what is actually pending without a second round trip.
|
||||
#
|
||||
# A null column and a stored payload with status 'unknown' both mean "never
|
||||
# scanned", which stays distinct from a scan that found nothing pending. The
|
||||
# status card relies on that distinction - see _dell_updates_field.
|
||||
dellupdatesjson = db.Column(db.Text(length=16777215), nullable=True)
|
||||
|
||||
# Warn-only reconciler output, refreshed on every agent check-in in the same
|
||||
# commit as servicesjson. Each entry names a policy/shipped-attached monitor
|
||||
# the endpoint is expected to be running but the latest snapshot does not
|
||||
|
||||
@@ -54,7 +54,7 @@ KNOWN_PAYLOAD_KEYS = frozenset({
|
||||
'scheduledtasks', 'applications', 'events',
|
||||
'disks', 'bios', 'memory', 'processor', 'osinfo', 'domain',
|
||||
'pendingreboot', 'usercontext', 'diskhealth',
|
||||
'rebootreasons', 'bootinfo', 'defender',
|
||||
'rebootreasons', 'bootinfo', 'defender', 'dellupdates',
|
||||
})
|
||||
|
||||
|
||||
@@ -182,12 +182,32 @@ class EndpointsPlugin(BasePlugin):
|
||||
# remote reboot is a strictly heavier grant than a routine toggle.
|
||||
('endpoints.restart',
|
||||
'Restart endpoint computers (destructive)', 'endpoints'),
|
||||
# Elevated grant for software delivery (run_script, update_app).
|
||||
# Elevated grant for software delivery (run_script, update_app) and
|
||||
# for starting approved scheduled tasks (scheduledtask_run).
|
||||
# Required IN ADDITION to endpoints.command by the enqueue route, so
|
||||
# running a staged script or pushing an installer is a strictly
|
||||
# heavier grant than a routine service/process toggle.
|
||||
#
|
||||
# NOTE FOR THE ROLES WORK - this wire is pre-built for you.
|
||||
# scheduledtask_run was deliberately mapped onto this EXISTING grant
|
||||
# rather than minting a new permission, so nothing needs migrating.
|
||||
# Be aware that WIDENS what endpoints.deploy means: it used to cover
|
||||
# run_script + update_app, and now also covers starting any task on
|
||||
# the agent's approved-task allowlist. Splitting it out is a one-line
|
||||
# change (add endpoints.corptools here and remap it in
|
||||
# commands.ELEVATED_ACTION_PERMISSIONS) - the enqueue route reads
|
||||
# that table, so no other code changes.
|
||||
#
|
||||
# If you do split it, note the three are NOT equal risk:
|
||||
# scheduledtask_run only starts something already registered on the
|
||||
# endpoint and is allowlisted agent-side; update_app runs a pinned
|
||||
# installer; run_script is arbitrary code that runs as SYSTEM unless
|
||||
# its descriptor pins the user context. run_script is the reason this
|
||||
# grant's description has to stay blunt.
|
||||
('endpoints.deploy',
|
||||
'Run staged scripts and push application installers to endpoints', 'endpoints'),
|
||||
'Run staged scripts, start approved scheduled tasks, and push '
|
||||
'application installers to endpoints (scripts run as SYSTEM; '
|
||||
'grant only to fully trusted operators)', 'endpoints'),
|
||||
# Packaging the agent is a heavier grant than deploying to one, and
|
||||
# it is not the same job either: the generated ZIP carries a live
|
||||
# collector key stamped into its install script, so whoever holds
|
||||
@@ -1113,6 +1133,13 @@ class EndpointsPlugin(BasePlugin):
|
||||
if 'defender' in payload:
|
||||
state.defenderjson = _as_json(payload.get('defender'))
|
||||
|
||||
# Presence-guarded like the probe output below: the agent omits the key
|
||||
# entirely when the collector utility is not installed, and overwriting
|
||||
# to null there would discard a still-valid scan result from before the
|
||||
# utility was removed.
|
||||
if 'dellupdates' in payload:
|
||||
state.dellupdatesjson = _as_json(payload.get('dellupdates'))
|
||||
|
||||
# Session-scoped probe output. The agent repeats its last result between
|
||||
# probe runs and omits the key entirely when nobody is signed in, so the
|
||||
# presence guard preserves the last known mapped drives across an
|
||||
|
||||
Reference in New Issue
Block a user