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.
85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
"""endpoint installed-software (SBOM) snapshot table.
|
|
|
|
Adds endpointapplications: one row per installed application discovered on an
|
|
endpoint, reported by the agent as {displayname, displayversion, publisher}.
|
|
Stored as a per-asset snapshot (replaced on each report), the same way the
|
|
services/ports inventories are kept as the newest list rather than history.
|
|
|
|
appid is groundwork for a future link to the core applications catalog (the
|
|
"Managed Applications" concept): a nullable FK with ON DELETE SET NULL so
|
|
retiring a managed Application does not delete discovered rows. No
|
|
discovery-to-catalog matcher exists yet; the column is laid down now so the
|
|
association can be added without a second migration.
|
|
|
|
Idempotent: creation checks the live schema first so a db.create_all() site
|
|
(which already built the table) re-runs clean on strict MySQL 8.
|
|
"""
|
|
from alembic import op
|
|
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision = 'endpoints0004apps'
|
|
down_revision = 'endpoints0003cmdtarget'
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def _has_table(table_name):
|
|
from sqlalchemy import inspect
|
|
|
|
return inspect(op.get_bind()).has_table(table_name)
|
|
|
|
|
|
def upgrade():
|
|
if _has_table('endpointapplications'):
|
|
return
|
|
|
|
op.create_table(
|
|
'endpointapplications',
|
|
sa.Column('endpointapplicationid', sa.Integer(), nullable=False),
|
|
sa.Column('assetid', sa.Integer(), nullable=False),
|
|
sa.Column('displayname', sa.String(length=255), nullable=False),
|
|
sa.Column('displayversion', sa.String(length=128), nullable=True),
|
|
sa.Column('publisher', sa.String(length=255), nullable=True),
|
|
sa.Column('appid', sa.Integer(), nullable=True),
|
|
sa.Column('recordedat', sa.DateTime(), nullable=False),
|
|
sa.PrimaryKeyConstraint('endpointapplicationid'),
|
|
sa.ForeignKeyConstraint(
|
|
['assetid'], ['assets.assetid'],
|
|
name='fk_endpointapplications_assetid',
|
|
ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(
|
|
['appid'], ['applications.appid'],
|
|
name='fk_endpointapplications_appid',
|
|
ondelete='SET NULL'),
|
|
)
|
|
op.create_index(
|
|
'idx_endpointapplications_assetid',
|
|
'endpointapplications', ['assetid'])
|
|
op.create_index(
|
|
'idx_endpointapplications_appid',
|
|
'endpointapplications', ['appid'])
|
|
op.create_index(
|
|
'idx_endpointapplications_displayname',
|
|
'endpointapplications', ['displayname'])
|
|
op.create_index(
|
|
'idx_endpointapplications_assetname',
|
|
'endpointapplications', ['assetid', 'displayname'])
|
|
|
|
|
|
def downgrade():
|
|
if not _has_table('endpointapplications'):
|
|
return
|
|
|
|
op.drop_index('idx_endpointapplications_assetname',
|
|
table_name='endpointapplications')
|
|
op.drop_index('idx_endpointapplications_displayname',
|
|
table_name='endpointapplications')
|
|
op.drop_index('idx_endpointapplications_appid',
|
|
table_name='endpointapplications')
|
|
op.drop_index('idx_endpointapplications_assetid',
|
|
table_name='endpointapplications')
|
|
op.drop_table('endpointapplications')
|