Three findings, all about a screen or a log saying something that is not so. Editing a PC wrote an audit entry for its named fields and nothing for the two changes that matter most when somebody investigates later: the primary IP address, which is how the machine is found, and the remote-access protocols and ports, which is how somebody connects to it. Both are on the same form, in the same request, and both were invisible afterwards - a PC that moved address had no history of moving, and a PC that gained a VNC listener had no record of gaining one. Both are recorded now, and re-sending the same value still records nothing, because an audit log full of non-events is one nobody reads. While proving that, a silent discard turned up in the same lines: both the create and the update path did `if ip_comtype:` around the IP write and did nothing at all when the 'IP' communication type row was missing. Somebody typed an address, saved, got a 200, and the address was gone. A missing reference row is a seeding gap, not a reason to drop what was typed, so it is created on demand. The PCs list rendered a 403 as "No computers found", which tells a person without the permission that the fleet is empty and sends them looking for the data rather than for the permission. It now says which permission is missing. The Site Policies and Configuration Policies pages rendered their empty table and an Add button while the policy feature flag was off - every route answers 404 "Policy feature is not enabled", so the button could never work. Both pages now say the feature is off and that the empty list is a consequence of that, not of nothing being configured. Nine tests. The audit entries verified by planting: remove either and two fail. Scope: product
105 lines
4.1 KiB
Python
105 lines
4.1 KiB
Python
"""Changing how a PC is reached has to leave a record.
|
|
|
|
Editing a computer wrote an audit entry for its named fields and nothing at all
|
|
for two changes that matter most to anybody investigating later:
|
|
|
|
* the primary IP address - how the PC is found on the network
|
|
* the remote-access protocols and their ports - how somebody connects to it
|
|
|
|
Both are set on the same form, in the same request, and both were invisible
|
|
afterwards. A PC that moved address had no history of moving, and a PC that
|
|
gained a VNC listener had no record of gaining one.
|
|
"""
|
|
import pytest
|
|
|
|
from shopdb.api import db
|
|
|
|
|
|
@pytest.fixture
|
|
def pc(client, db, auth_headers):
|
|
from shopdb.core.models import AssetType, AssetStatus
|
|
|
|
if not AssetType.query.filter_by(assettype='computer').first():
|
|
db.session.add(AssetType(assettype='computer'))
|
|
if not AssetStatus.query.first():
|
|
db.session.add(AssetStatus(status='In Use'))
|
|
db.session.commit()
|
|
|
|
created = client.post('/api/computers', headers=auth_headers, json={
|
|
'assetnumber': 'AUDIT-PC-1',
|
|
'hostname': 'auditpc1',
|
|
'ipaddress': '10.20.30.40',
|
|
})
|
|
assert created.status_code == 201, created.get_json()
|
|
data = created.get_json()['data']
|
|
return data.get('computerid') or data['computer']['computerid']
|
|
|
|
|
|
def _entries(entityid):
|
|
from shopdb.core.models import AuditLog
|
|
|
|
return (AuditLog.query
|
|
.filter_by(entitytype='Computer', entityid=entityid)
|
|
.order_by(AuditLog.auditlogid.desc()).all())
|
|
|
|
|
|
def test_moving_a_pc_to_another_address_is_recorded(client, auth_headers, pc):
|
|
response = client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'ipaddress': '10.20.30.99'})
|
|
assert response.status_code == 200, response.get_json()
|
|
|
|
latest = _entries(pc)[0]
|
|
assert 'ipaddress' in (latest.changes or {}), latest.changes
|
|
assert latest.changes['ipaddress']['old'] == '10.20.30.40'
|
|
assert latest.changes['ipaddress']['new'] == '10.20.30.99'
|
|
|
|
|
|
def test_an_unchanged_address_records_nothing(client, auth_headers, pc):
|
|
"""A PUT that re-sends the same value is not a change, and an audit log
|
|
full of non-events is one nobody reads."""
|
|
before = len(_entries(pc))
|
|
response = client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'ipaddress': '10.20.30.40'})
|
|
assert response.status_code == 200
|
|
assert len(_entries(pc)) == before
|
|
|
|
|
|
def test_changing_the_remote_access_protocols_is_recorded(client, db,
|
|
auth_headers, pc):
|
|
from plugins.computers.models import AccessProtocol
|
|
|
|
rdp = AccessProtocol(name='RDP', scheme='rdp', linktemplate='rdp://{host}')
|
|
vnc = AccessProtocol(name='VNC', scheme='vnc', linktemplate='vnc://{host}')
|
|
db.session.add_all([rdp, vnc])
|
|
db.session.commit()
|
|
|
|
client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'accessmethods': [{'protocolid': rdp.protocolid}]})
|
|
response = client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'accessmethods': [
|
|
{'protocolid': vnc.protocolid,
|
|
'portoverride': 5999}]})
|
|
assert response.status_code == 200, response.get_json()
|
|
|
|
latest = _entries(pc)[0]
|
|
assert 'accessmethods' in (latest.changes or {}), latest.changes
|
|
change = latest.changes['accessmethods']
|
|
assert change['old'] == ['%d:default' % rdp.protocolid]
|
|
assert change['new'] == ['%d:5999' % vnc.protocolid]
|
|
|
|
|
|
def test_resending_the_same_protocols_records_nothing(client, db,
|
|
auth_headers, pc):
|
|
from plugins.computers.models import AccessProtocol
|
|
|
|
ssh = AccessProtocol(name='SSH', scheme='ssh', linktemplate='ssh://{host}')
|
|
db.session.add(ssh)
|
|
db.session.commit()
|
|
|
|
client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'accessmethods': [{'protocolid': ssh.protocolid}]})
|
|
before = len(_entries(pc))
|
|
client.put('/api/computers/%d' % pc, headers=auth_headers,
|
|
json={'accessmethods': [{'protocolid': ssh.protocolid}]})
|
|
assert len(_entries(pc)) == before
|