Report whether a returned drive was wiped and scanned
The USB audit answered who had which drive and when, but not whether it came back wiped or virus-scanned. That is the question the record is kept to answer: removable-media custody is CMMC evidence, and custody without the state of the media on return is half the story. Both stores already held the values - the external checkinoutlog in sanitized and scanned_viruses, the app's own usbcheckouts in waswiped and scannedviruses - and neither mode selected them. Both now report them, in the JSON, the CSV and the column list, so the two directory modes still answer in one voice. Two things the rendering has to get right: - Only a return carries them. The wipe and the scan happen when the drive comes back, so a check-out row shows blanks rather than implying a fact about an event that had not happened yet. - Unknown is not "no". Both stores keep these nullable deliberately: a null means nobody wrote it down, which is a different claim from a drive that came back unwiped. Rendering an unrecorded return as "No" would put an assertion in an audit record that no one ever made, so unknown stays blank and reads as the gap it is. A test holds that, because "no" is the tempting default. auditflag lives in selfhosted.py rather than routes.py: routes already imports selfhosted, so the helper had to sit on that side or the import would be circular.
This commit is contained in:
@@ -1209,7 +1209,8 @@ def _auditcsv(rows, columns, filename):
|
||||
|
||||
|
||||
AUDITCOLUMNS = ['timestamp', 'action', 'device_id', 'device_desc', 'badge',
|
||||
'name', 'locker_location']
|
||||
'name', 'locker_location', 'sanitized', 'scanned_viruses']
|
||||
|
||||
|
||||
|
||||
@usb_bp.route('/reports/audit', methods=['GET'])
|
||||
@@ -1235,7 +1236,8 @@ def report_audit():
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
sql = ('SELECT l.timestamp, l.action, l.device_id, l.badge_number, '
|
||||
'l.locker_location, d.device_desc, '
|
||||
'l.locker_location, l.sanitized, l.scanned_viruses, '
|
||||
'd.device_desc, '
|
||||
'u.first_name, u.last_name '
|
||||
'FROM checkinoutlog l '
|
||||
'LEFT JOIN devices d ON d.device_id = l.device_id '
|
||||
@@ -1263,6 +1265,13 @@ def report_audit():
|
||||
'badge': row.get('badge_number') or '',
|
||||
'name': name,
|
||||
'locker_location': row.get('locker_location') or '',
|
||||
# Only a return can carry these: the wipe and the scan
|
||||
# happen when the drive comes back, so a check-out row
|
||||
# showing them would invent a fact.
|
||||
'sanitized': (selfhosted.auditflag(row.get('sanitized'))
|
||||
if row.get('action') == 'check-in' else ''),
|
||||
'scanned_viruses': (selfhosted.auditflag(row.get('scanned_viruses'))
|
||||
if row.get('action') == 'check-in' else ''),
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -544,6 +544,22 @@ def _kiosk_flag(key, default):
|
||||
return str(row.value).strip().lower() in ('true', '1', 'yes', 'on')
|
||||
|
||||
|
||||
def auditflag(value):
|
||||
"""Render a stored tri-state as 'yes', 'no', or '' for the report.
|
||||
|
||||
THREE states, not two. Both stores keep these nullable on purpose: a null
|
||||
means nobody recorded it, which is not the same claim as 'no'. Rendering an
|
||||
unrecorded return as 'No' would put a assertion in an audit record that
|
||||
nobody ever made - the opposite of what the record is for. So unknown stays
|
||||
blank and reads as the gap it is.
|
||||
"""
|
||||
if value is None or value == '':
|
||||
return ''
|
||||
if isinstance(value, str):
|
||||
return 'yes' if value.strip() in ('1', 'true', 'True') else 'no'
|
||||
return 'yes' if value else 'no'
|
||||
|
||||
|
||||
def audit_rows(days):
|
||||
"""Check-out / check-in events over a window (self-hosted mode).
|
||||
|
||||
@@ -576,11 +592,16 @@ def audit_rows(days):
|
||||
'locker_location': device.storagelocation or '',
|
||||
}
|
||||
if checkout.checkouttime and (cutoff is None or checkout.checkouttime >= cutoff):
|
||||
# No wipe or scan state on a take: both are recorded on the return,
|
||||
# so putting them here would invent a fact.
|
||||
rows.append(dict(base, timestamp=checkout.checkouttime.isoformat(),
|
||||
action='check-out'))
|
||||
action='check-out',
|
||||
sanitized='', scanned_viruses=''))
|
||||
if checkout.checkintime and (cutoff is None or checkout.checkintime >= cutoff):
|
||||
rows.append(dict(base, timestamp=checkout.checkintime.isoformat(),
|
||||
action='check-in'))
|
||||
action='check-in',
|
||||
sanitized=auditflag(checkout.waswiped),
|
||||
scanned_viruses=auditflag(checkout.scannedviruses)))
|
||||
|
||||
# Newest first, and a total order so two events in the same second do not
|
||||
# swap places between identical runs.
|
||||
|
||||
@@ -137,3 +137,66 @@ def test_the_report_is_registered_so_it_appears_in_the_catalogue(client, usbdata
|
||||
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'
|
||||
|
||||
|
||||
# -- sanitized / virus-scan state in the audit ---------------------------------
|
||||
# CMMC evidence: the report answered who had which drive and when, but not
|
||||
# whether it was wiped or scanned on the way back - which is the question the
|
||||
# record exists to answer. Both stores held the values; neither reported them.
|
||||
|
||||
def test_audit_reports_wipe_and_scan_state_on_returns(db, client, auth_headers):
|
||||
from plugins.usb.models import USBDevice, USBCheckout
|
||||
from shopdb.core.models import Setting
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
db.session.add(Setting(key='usb_directory_mode', value='selfhosted',
|
||||
valuetype='string', category='site'))
|
||||
device = USBDevice(serialnumber='USB-9', label='Grey SanDisk')
|
||||
db.session.add(device)
|
||||
db.session.flush()
|
||||
now = datetime.utcnow()
|
||||
db.session.add(USBCheckout(
|
||||
usbdeviceid=device.usbdeviceid, sso='270015376', checkoutname='Dana',
|
||||
checkouttime=now - timedelta(hours=2), checkintime=now - timedelta(hours=1),
|
||||
waswiped=True, scannedviruses=False))
|
||||
db.session.commit()
|
||||
|
||||
response = client.get('/api/usb/reports/audit?days=7', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()['data']
|
||||
assert 'sanitized' in data['columns']
|
||||
assert 'scanned_viruses' in data['columns']
|
||||
|
||||
checkin = [r for r in data['rows'] if r['action'] == 'check-in'][0]
|
||||
checkout = [r for r in data['rows'] if r['action'] == 'check-out'][0]
|
||||
assert checkin['sanitized'] == 'yes'
|
||||
assert checkin['scanned_viruses'] == 'no'
|
||||
# A take carries neither: both are recorded when the drive comes back.
|
||||
assert checkout['sanitized'] == ''
|
||||
assert checkout['scanned_viruses'] == ''
|
||||
|
||||
|
||||
def test_unrecorded_wipe_is_blank_not_no(db, client, auth_headers):
|
||||
"""A null means nobody wrote it down. Rendering that as 'no' would assert
|
||||
something no one ever claimed, in the one record kept to be trustworthy."""
|
||||
from plugins.usb.models import USBDevice, USBCheckout
|
||||
from shopdb.core.models import Setting
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
db.session.add(Setting(key='usb_directory_mode', value='selfhosted',
|
||||
valuetype='string', category='site'))
|
||||
device = USBDevice(serialnumber='USB-10', label='Old stock')
|
||||
db.session.add(device)
|
||||
db.session.flush()
|
||||
now = datetime.utcnow()
|
||||
db.session.add(USBCheckout(
|
||||
usbdeviceid=device.usbdeviceid, sso='270015376', checkoutname='Dana',
|
||||
checkouttime=now - timedelta(hours=3), checkintime=now - timedelta(hours=2),
|
||||
waswiped=None, scannedviruses=None))
|
||||
db.session.commit()
|
||||
|
||||
response = client.get('/api/usb/reports/audit?days=7', headers=auth_headers)
|
||||
checkin = [r for r in response.get_json()['data']['rows']
|
||||
if r['action'] == 'check-in'][0]
|
||||
assert checkin['sanitized'] == ''
|
||||
assert checkin['scanned_viruses'] == ''
|
||||
|
||||
Reference in New Issue
Block a user