140 lines
5.5 KiB
Python
140 lines
5.5 KiB
Python
"""USB check-in / check-out audit report.
|
|
|
|
CMMC removable-media tracking is an audit obligation, and the question an audit
|
|
asks is "who had this drive, when" - which a total cannot answer. So this
|
|
reports EVENTS over a window, with the window named rather than expressed as a
|
|
day count nobody should have to work out.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from shopdb.core.models import Setting
|
|
|
|
|
|
def _now():
|
|
return datetime.now(timezone.utc).replace(tzinfo=None)
|
|
|
|
|
|
@pytest.fixture
|
|
def usbdata(db):
|
|
"""Self-hosted USB with a loan returned today and one from long ago."""
|
|
from plugins.usb.models import USBDevice, USBCheckout
|
|
|
|
db.session.add(Setting(key='usb_directory_mode', value='selfhosted',
|
|
valuetype='string', category='site'))
|
|
recent = USBDevice(serialnumber='USB-1', label='Blue Kingston')
|
|
old = USBDevice(serialnumber='USB-2', label='Red SanDisk')
|
|
db.session.add_all([recent, old])
|
|
db.session.flush()
|
|
|
|
# Taken and returned today: two events.
|
|
db.session.add(USBCheckout(usbdeviceid=recent.usbdeviceid, sso='270015376',
|
|
checkoutname='Dana Reyes',
|
|
checkouttime=_now() - timedelta(hours=3),
|
|
checkintime=_now() - timedelta(hours=1)))
|
|
# Taken 200 days ago and never returned: one event, outside most windows.
|
|
db.session.add(USBCheckout(usbdeviceid=old.usbdeviceid, sso='270099999',
|
|
checkoutname='Sam Okafor',
|
|
checkouttime=_now() - timedelta(days=200)))
|
|
db.session.commit()
|
|
return db
|
|
|
|
|
|
def _report(client, headers, **params):
|
|
query = '&'.join(f'{k}={v}' for k, v in params.items())
|
|
response = client.get(f'/api/usb/reports/audit?{query}', headers=headers)
|
|
assert response.status_code == 200, response.get_json()
|
|
return response.get_json()['data']
|
|
|
|
|
|
def test_a_loan_produces_both_a_take_and_a_return(client, usbdata, auth_headers):
|
|
"""One stored row holds both ends of a loan.
|
|
|
|
Reporting it once would make a returned drive look identical to one still
|
|
out, which is the opposite of what the log is for.
|
|
"""
|
|
data = _report(client, auth_headers, period='month')
|
|
actions = sorted(row['action'] for row in data['rows'])
|
|
|
|
assert actions == ['check-in', 'check-out']
|
|
assert data['summary']['checkouts'] == 1
|
|
assert data['summary']['checkins'] == 1
|
|
|
|
|
|
def test_the_window_actually_bounds_the_rows(client, usbdata, auth_headers):
|
|
month = _report(client, auth_headers, period='month')
|
|
year = _report(client, auth_headers, period='year')
|
|
|
|
assert {row['device_id'] for row in month['rows']} == {'USB-1'}
|
|
assert {row['device_id'] for row in year['rows']} == {'USB-1', 'USB-2'}
|
|
|
|
|
|
def test_all_time_keeps_everything(client, usbdata, auth_headers):
|
|
data = _report(client, auth_headers, period='all')
|
|
assert data['days'] == 0
|
|
assert data['summary']['events'] == 3
|
|
|
|
|
|
def test_today_is_a_window_too(client, usbdata, auth_headers):
|
|
"""A shift handover asks what happened today, not this month."""
|
|
data = _report(client, auth_headers, period='day')
|
|
assert data['days'] == 1
|
|
assert data['summary']['events'] == 2
|
|
|
|
|
|
@pytest.mark.parametrize('period,days', [('day', 1), ('month', 30),
|
|
('sixmonths', 182), ('year', 365),
|
|
('all', 0)])
|
|
def test_every_named_window_maps_to_a_day_count(client, usbdata, auth_headers,
|
|
period, days):
|
|
assert _report(client, auth_headers, period=period)['days'] == days
|
|
|
|
|
|
def test_an_unknown_period_falls_back_rather_than_failing(client, usbdata,
|
|
auth_headers):
|
|
"""A bad query string should not 500 a report somebody is mid-audit on."""
|
|
assert _report(client, auth_headers, period='fortnight')['days'] == 30
|
|
|
|
|
|
def test_rows_are_newest_first(client, usbdata, auth_headers):
|
|
rows = _report(client, auth_headers, period='all')['rows']
|
|
stamps = [row['timestamp'] for row in rows]
|
|
assert stamps == sorted(stamps, reverse=True)
|
|
|
|
|
|
def test_it_says_who_and_which_drive(client, usbdata, auth_headers):
|
|
rows = _report(client, auth_headers, period='month')['rows']
|
|
row = rows[0]
|
|
assert row['name'] == 'Dana Reyes'
|
|
assert row['badge'] == '270015376'
|
|
assert row['device_id'] == 'USB-1'
|
|
assert row['device_desc'] == 'Blue Kingston'
|
|
|
|
|
|
def test_it_exports_as_csv(client, usbdata, auth_headers):
|
|
response = client.get('/api/usb/reports/audit?period=all&format=csv',
|
|
headers=auth_headers)
|
|
assert response.status_code == 200
|
|
assert response.mimetype == 'text/csv'
|
|
body = response.get_data(as_text=True)
|
|
assert body.splitlines()[0].startswith('timestamp,action,device_id')
|
|
assert 'Dana Reyes' in body
|
|
|
|
|
|
def test_reading_the_audit_needs_permission(client, usbdata, member_headers):
|
|
"""A role-less member cannot read who had which drive."""
|
|
response = client.get('/api/usb/reports/audit?period=all',
|
|
headers=member_headers)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_the_report_is_registered_so_it_appears_in_the_catalogue(client, usbdata,
|
|
auth_headers):
|
|
from plugins.usb.plugin import USBPlugin
|
|
report = USBPlugin().get_reports()[0]
|
|
assert report['id'] == 'usb-audit'
|
|
# The endpoint is what the reports page fetches; a wrong one renders blank.
|
|
assert report['endpoint'] == '/api/usb/reports/audit'
|