Clear the review blockers on the merged wilm work
A multi-agent review of the merged branch produced 29 findings that survived
adversarial verification. This clears the blockers and the four worth fixing
before it reaches main. The design held up well: path-traversal defence on all
four serving routes is clean, the agent's SHA-256 check fails closed, enqueue
authority is proportionate and audited, and no site literals or credential leaks
in the new C#.
THE TESTS NEVER RAN
pytest.ini sets testpaths = tests, so plugins/endpoints/tests/ was collected ZERO
times. Fifty-five lines of delivery assertions were dead code, and run directly
five of them failed. plugins/printedparts/tests/ had been invisible for longer.
That is the worst shape a gap can take: the suite is green, the coverage is
imaginary, and the collector auth on a download route could be deleted without
anything going red.
Both files moved under tests/. tests/test_no_hidden_tests.py now fails if a test
file appears anywhere pytest will not look; it caught a staged copy inside the
installer bundle on its first run, and was verified by planting a violation.
The five failures were stale expectations, and they split by cause. His: the
applications panel made six panels where his test said five, and 'applications'
belongs in KNOWN_PAYLOAD_KEYS now that it is persisted - his own test contradicted
his own feature. Ours: we renamed the events card to activity, moved the
fleet-wide cards to endpoints.fleet, and made the list route a bare route('')
which Flask rejects unless the blueprint is mounted under a prefix.
FOUR FIXES
The snapshot table's key was a 32-bit autoincrement on a table rewritten
wholesale every check-in, so it climbs at (endpoints x apps) per report and
exhausts on MySQL. Widened while the table is unreleased. Widening it broke every
insert on SQLite, which only autoincrements a column typed exactly INTEGER
PRIMARY KEY, so it carries a dialect variant - and nothing caught either fault,
because no test inserted into that table at all. One does now.
Ingest truncated displayname but not displayversion or publisher. On strict MySQL
an over-long registry value fails the whole check-in transaction, so one machine
with a malformed Uninstall entry goes silent entirely. SQLite does not enforce
lengths, so the suite would never have shown it.
update_app's version: branch pinned AppVersion.sha256, but that route resolves the
version to its APPLICATION and serves application.installpath - so the digest
described an artifact the route never sends, and where it was null the agent had
nothing to verify and failed open. It now pins the file actually served.
The same branch ended the delivery URL at the id, so an MSI arrived with no
extension and could not be routed through msiexec. It carries the filename now.
Agent packaging moved off require_role('admin') to a new endpoints.agentpackage
permission. The generated ZIP carries a live collector key, so this is a heavier
grant than deploying to one endpoint - and a role cannot be given to one person
for one job the way a permission can.
Full suite 2231 passed (2198 before, plus the 22 that never ran and 11 new),
frontend 314.
This commit is contained in:
87
tests/test_plugins/test_endpoint_applications_model.py
Normal file
87
tests/test_plugins/test_endpoint_applications_model.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""The installed-software snapshot table: it must actually accept inserts.
|
||||
|
||||
This table is rewritten wholesale on every endpoint check-in - delete the
|
||||
asset's rows, insert the current inventory - so its surrogate key climbs at
|
||||
(endpoints x installed apps) per report and never reuses a value. On MySQL a
|
||||
32-bit key exhausts and inserts stop working permanently.
|
||||
|
||||
Widening it to BigInteger broke every insert on SQLite, because SQLite only
|
||||
autoincrements a column typed exactly INTEGER PRIMARY KEY. Neither fault was
|
||||
caught by the suite, for the same reason: nothing inserted into this table at
|
||||
all. This does.
|
||||
"""
|
||||
from shopdb.api import db
|
||||
from plugins.endpoints.models.endpoint import EndpointApplication
|
||||
|
||||
|
||||
def test_rows_insert_and_the_key_autoincrements(app):
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
first = EndpointApplication(assetid=1, displayname='Probe', displayversion='1.0')
|
||||
second = EndpointApplication(assetid=1, displayname='Probe 2', displayversion='2.0')
|
||||
db.session.add_all([first, second])
|
||||
db.session.commit()
|
||||
|
||||
assert first.endpointapplicationid is not None
|
||||
assert second.endpointapplicationid is not None
|
||||
assert second.endpointapplicationid != first.endpointapplicationid
|
||||
|
||||
|
||||
def test_the_key_is_wide_enough_for_a_table_that_is_rewritten_every_cycle(app):
|
||||
"""BIGINT on MySQL, INTEGER on SQLite - both halves matter.
|
||||
|
||||
Dropping the variant breaks SQLite inserts; dropping the BigInteger breaks
|
||||
MySQL years later, which is worse because it is silent until it is not.
|
||||
"""
|
||||
from sqlalchemy.dialects import mysql, sqlite
|
||||
|
||||
with app.app_context():
|
||||
column = EndpointApplication.__table__.c.endpointapplicationid
|
||||
assert 'BIGINT' in str(column.type.compile(dialect=mysql.dialect()))
|
||||
assert 'INTEGER' in str(column.type.compile(dialect=sqlite.dialect()))
|
||||
|
||||
|
||||
def test_over_long_registry_values_are_clamped_not_fatal(app):
|
||||
"""One malformed Uninstall entry must not cost the whole check-in.
|
||||
|
||||
On MySQL in strict mode an over-long value raises Data too long and fails the
|
||||
transaction, taking telemetry and events down with it - so that endpoint goes
|
||||
silent until somebody uninstalls the offending program. SQLite does not
|
||||
enforce declared lengths, so the suite would never have shown it.
|
||||
"""
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
warnings = []
|
||||
stored = EndpointsPlugin()._replace_applications(
|
||||
assetid=4242,
|
||||
applications=[{
|
||||
'displayname': 'A' * 400,
|
||||
'displayversion': 'V' * 400,
|
||||
'publisher': 'P' * 400,
|
||||
}],
|
||||
warnings=warnings,
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
assert stored == 1
|
||||
row = EndpointApplication.query.filter_by(assetid=4242).one()
|
||||
assert len(row.displayname) == 255
|
||||
assert len(row.displayversion) == 128
|
||||
assert len(row.publisher) == 255
|
||||
|
||||
|
||||
def test_a_blank_value_becomes_null_rather_than_an_empty_string(app):
|
||||
from plugins.endpoints.plugin import EndpointsPlugin
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
EndpointsPlugin()._replace_applications(
|
||||
assetid=4243,
|
||||
applications=[{'displayname': 'Thing', 'publisher': ' '}],
|
||||
warnings=[],
|
||||
)
|
||||
db.session.commit()
|
||||
row = EndpointApplication.query.filter_by(assetid=4243).one()
|
||||
assert row.publisher is None
|
||||
Reference in New Issue
Block a user