Merge the wilm delivery work into main

Ben Baker's installed-software collection, software delivery (run_script and
update_app), agentless adoption tool and command-queue hardening, with his
authorship preserved.

Reviewed before promotion. Fixed: the endpoints plugin tests were never collected
by pytest, the SBOM snapshot table's key would exhaust on MySQL, ingest did not
truncate two columns strict MySQL would reject, and update_app's version branch
pinned a digest for a file that route never serves. Agent packaging moved off
require_role('admin') onto a new endpoints.agentpackage permission.

Contract 0.27.0. Full suite 2231, frontend 314.
This commit is contained in:
2026-08-30 19:41:15 -04:00
parent 8f4fe74dcb
commit 6680c3c5de
59 changed files with 5557 additions and 68 deletions

View File

@@ -16,6 +16,8 @@ from .communication import Communication, CommunicationType
from .user import (User, Role, Permission, full_permission_catalog,
guest_grantable_permissions)
from .application import Application, AppVersion
from .script_library import (Script, RUNCONTEXTS, RUNCONTEXT_SYSTEM,
RUNCONTEXT_USER)
from .supportteam import SupportTeam, SupportTeamContact
from .setting import Setting
from .auditlog import AuditLog
@@ -62,6 +64,11 @@ __all__ = [
# Applications
'Application',
'AppVersion',
# Script library (endpoint run_script delivery)
'Script',
'RUNCONTEXTS',
'RUNCONTEXT_SYSTEM',
'RUNCONTEXT_USER',
# Support teams
'SupportTeam',
'SupportTeamContact',

View File

@@ -34,6 +34,11 @@ class Application(BaseModel):
comment='Required on all PCs (drives the software-compliance report)'
)
image = db.Column(db.String(255))
# Digest of the app-level uploaded installer (installpath). Lets the
# endpoint update_app push resolve an install source by app alone, with no
# AppVersion row: the agent fetches the pinned bytes and verifies this hash.
# Stamped on package upload; null when installpath is a site share path.
installersha256 = db.Column(db.String(64))
# Relationships
supportteam = db.relationship('SupportTeam', back_populates='applications')
@@ -70,6 +75,12 @@ class AppVersion(db.Model):
notes = db.Column(db.String(255))
dateadded = db.Column(db.DateTime, default=_utcnow)
isactive = db.Column(db.Boolean, default=True)
# Delivery metadata for the endpoint installer push. sha256 pins the exact
# bytes the agent must fetch (warn-not-block for now; hardened later);
# silentargs carries this version's unattended-install switches because
# homegrown EXEs vary by developer (MSI is uniform via msiexec).
sha256 = db.Column(db.String(64))
silentargs = db.Column(db.String(255))
# Relationships
application = db.relationship('Application', back_populates='versions')
@@ -88,7 +99,9 @@ class AppVersion(db.Model):
'releasedate': self.releasedate.isoformat() if self.releasedate else None,
'notes': self.notes,
'dateadded': self.dateadded.isoformat() + 'Z' if self.dateadded else None,
'isactive': self.isactive
'isactive': self.isactive,
'sha256': self.sha256,
'silentargs': self.silentargs
}
def __repr__(self):

View File

@@ -0,0 +1,43 @@
"""ShopDB-hosted script library for endpoint run_script delivery.
A script is authored/uploaded once here, and the endpoint agent fetches its
body by digest on push (mirroring the application installer delivery path). The
body never lives on the endpoint ahead of time and never crosses the command
wire; the agent downloads it with its collector credential and verifies sha256.
"""
from shopdb.extensions import db
from .base import BaseModel
# Execution context for a delivered script. SYSTEM is the service account the
# agent already runs as (no user session, cannot see mapped drives); USER runs
# in the interactive logged-on user's session. Default SYSTEM stays fail-safe.
RUNCONTEXT_SYSTEM = 'system'
RUNCONTEXT_USER = 'user'
RUNCONTEXTS = (RUNCONTEXT_SYSTEM, RUNCONTEXT_USER)
class Script(BaseModel):
"""ShopDB-hosted PowerShell script the endpoint agent runs on push."""
__tablename__ = 'scripts'
scriptid = db.Column(db.Integer, primary_key=True)
scriptname = db.Column(db.String(100), unique=True, nullable=False)
scriptdescription = db.Column(db.String(255))
# Stored file name under the scripts upload directory (script-<id>.ps1). The
# body is served by the collector-authenticated download route.
filename = db.Column(db.String(255))
# Digest of the stored body; the agent verifies this before running.
sha256 = db.Column(db.String(64))
# 'system' or 'user' (see RUNCONTEXTS). Per-script so a drive-mapping script
# can target the user session while a machine-wide script runs as SYSTEM.
runcontext = db.Column(db.String(16), default=RUNCONTEXT_SYSTEM, nullable=False)
def to_dict(self):
result = super().to_dict()
# hasbody lets the UI disable pushing a script with no uploaded file.
result['hasbody'] = bool(self.filename and self.sha256)
return result
def __repr__(self):
return f"<Script {self.scriptname}>"