Persist and surface endpoint installed-software (SBOM)

The endpoint agent already reports an installed-software list (SBOM), but ingress dropped it: the collector schema had no entry for it and there was nowhere to store it, so operators could not see what is installed on a PC.

Persist it as a per-asset snapshot in a new endpointapplications table, replaced on every report (current inventory is what operators ask for, not install history). Add applications to the collector schema and KNOWN_PAYLOAD_KEYS so ingress stops discarding it, a read route for the raw collection, and a collapsed-by-default Installed software panel to surface it. appid is nullable groundwork for a future Managed Applications link; no matcher is wired yet.
This commit is contained in:
benjamin.baker
2026-08-30 11:20:45 -04:00
parent 345c7cefff
commit 9725c47814
7 changed files with 290 additions and 10 deletions

View File

@@ -21,7 +21,7 @@ from shopdb.api import (
AuditLog, collector_authorized, User,
)
from ..models import EndpointState, EndpointEvent, EndpointCommand
from ..models import EndpointState, EndpointEvent, EndpointCommand, EndpointApplication
from ..commands import COMMAND_ALLOWLIST, is_allowed_action, ALLOWED_ACTIONS_FOR, row_actions_for, requires_target, action_targetkind, elevated_permission_for
from ..eventcatalog import CRITICAL_EVENT_IDS, event_severity, event_title
@@ -609,6 +609,32 @@ def panel_interfaces(asset_id: int):
return success_response(rows)
@endpoints_bp.route('/asset/<int:asset_id>/applications', methods=['GET'])
@require_permission('endpoints.view')
def panel_applications(asset_id: int):
"""Table panel: the installed-software (SBOM) snapshot for this endpoint.
Reads the endpointapplications rows the collector last wrote for this asset
(a per-report snapshot, not history) and returns them sorted by name. This
is the raw discovered inventory: appid is carried through but not yet linked
to the applications catalog ("Managed Applications"), so no managed status
is surfaced here. An endpoint with no reported SBOM yields no rows and the
panel hides itself.
"""
rows = (EndpointApplication.query
.filter_by(assetid=asset_id)
.order_by(db.func.lower(EndpointApplication.displayname))
.all())
return success_response([
{
'displayname': row.displayname,
'displayversion': row.displayversion or '-',
'publisher': row.publisher or '-',
}
for row in rows
])
# =============================================================================
# Dashboard
# =============================================================================