USB check-in / check-out audit, as a report rather than a paged list

CMMC removable-media tracking is an audit obligation, and answering "who had
this drive, and when" meant paging through the check-in/out list by hand.
GET /api/usb/reports/audit returns the events - a take and a return are separate
rows, because a count cannot answer the question an audit actually asks - with
summary figures riding along for the people who only want the shape of it, and
a CSV for the people who want it in a spreadsheet.

The window is named rather than a day count: today, last month, six months, a
year, all time. Nobody should have to work out that a year is 365.

Both directory modes answer in one voice. The external path reads the
cmmc_usb checkinoutlog; self-hosted expands one usbcheckouts row into its two
ends, so a returned drive is distinguishable from one still out, and the window
filter runs in SQL against either end of the loan - a drive taken before the
window and returned inside it is a check-in the window has to show. Rows come
back from the mode-specific half; the CSV, the summary and the response shape
are built once in the route.

Ordering is total in both modes. Timestamp alone ties for two events in the same
second, and the same query then returns them in a different order each run,
which reads as the log being unstable.
This commit is contained in:
cproudlock
2026-08-27 10:20:21 -04:00
parent add310e4e9
commit f59aca5a1b
8 changed files with 388 additions and 5 deletions

View File

@@ -70,6 +70,26 @@ class USBPlugin(BasePlugin):
'docs': 'plugins/usb/README.md',
}
def get_reports(self) -> List[Dict]:
"""Removable-media audit.
CMMC tracking is an audit obligation, and the only way to answer "who
had which drive, when" was to page through the check-in/out list by
hand. The window covers a shift, a periodic review, or everything ever
recorded.
"""
return [
{
'id': 'usb-audit',
'name': 'USB Check-in / Check-out Audit',
'description': 'Every take and return over a chosen window - '
'day, month, six months, year or all time - with '
'who, which drive and when',
'category': 'usage',
'endpoint': '/api/usb/reports/audit',
},
]
def get_settings_cards(self) -> List[Dict]:
"""Somewhere to actually turn the kiosk on.