Skip to content

Commit 004447a

Browse files
committed
Add compliance history view per subject/scope
Adds a new /page/history/{subject}/{scopeuuid} endpoint (and markdown/ fragment variants) that lists all past compliance reports for a given subject and scope, newest first, with per-report PASS/FAIL/ABORT counts and links to each individual report. Also adds a "compliance history" link to the detail pages, a history_url() helper in render_view(), and fixes a pre-existing TypeError crash on fresh databases in db_upgrade_schema() when the schema version is None. Signed-off-by: Thomas Güttler <thomas.guettler@syself.com>
1 parent f76ee40 commit 004447a

5 files changed

Lines changed: 64 additions & 4 deletions

File tree

compliance-monitor/monitor.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
db_get_keys, db_insert_report, db_get_recent_results2, db_patch_approval2, db_get_report,
4343
db_ensure_schema, db_get_apikeys, db_update_apikey, db_filter_apikeys, db_clear_delegates,
4444
db_find_subjects, db_insert_result2, db_get_relevant_results2, db_add_delegate, db_get_group,
45-
db_filter_accounts,
45+
db_filter_accounts, db_get_report_history,
4646
)
4747

4848

@@ -84,6 +84,7 @@ def __init__(self):
8484
ROLES = {'read_any': 1, 'append_any': 2, 'admin': 4, 'approve': 8}
8585
# number of days that expired results will be considered in lieu of more recent, but unapproved ones
8686
GRACE_PERIOD_DAYS = 7
87+
HISTORY_LIMIT = 500
8788
# separator between signature and report data; use something like
8889
# ssh-keygen \
8990
# -Y sign -f ~/.ssh/id_ed25519 -n report myreport.yaml
@@ -122,7 +123,12 @@ class ViewType(Enum):
122123
ViewType.fragment: 'scope.md',
123124
ViewType.page: 'overview.html',
124125
}
125-
REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE) for fn in view.values()))
126+
VIEW_HISTORY = {
127+
ViewType.markdown: 'history.md',
128+
ViewType.fragment: 'history.md',
129+
ViewType.page: 'overview.html',
130+
}
131+
REQUIRED_TEMPLATES = tuple(set(fn for view in (VIEW_REPORT, VIEW_DETAIL, VIEW_TABLE, VIEW_SCOPE, VIEW_HISTORY) for fn in view.values()))
126132

127133

128134
# do I hate these globals, but I don't see another way with these frameworks
@@ -580,8 +586,9 @@ def render_view(view, view_type, detail_page='detail', base_url='/', title=None,
580586
stage1 = view[ViewType.fragment]
581587
def scope_url(uuid): return f"{base_url}page/scope/{uuid}" # noqa: E306,E704
582588
def detail_url(subject, scope): return f"{base_url}page/{detail_page}/{subject}/{scope}" # noqa: E306,E704
589+
def history_url(subject, scope): return f"{base_url}page/history/{subject}/{scope}" # noqa: E306,E704
583590
def report_url(report, *args, **kwargs): return _build_report_url(base_url, report, *args, **kwargs) # noqa: E306,E704
584-
fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, report_url=report_url, scope_url=scope_url, **kwargs)
591+
fragment = templates_map[stage1].render(base_url=base_url, detail_url=detail_url, history_url=history_url, report_url=report_url, scope_url=scope_url, **kwargs)
585592
if view_type != ViewType.markdown and stage1.endswith('.md'):
586593
fragment = markdown(fragment, extensions=['extra'])
587594
if stage1 != stage2:
@@ -702,6 +709,24 @@ async def get_detail_full(
702709
)
703710

704711

712+
@app.get("/{view_type}/history/{subject}/{scopeuuid}")
713+
async def get_history(
714+
request: Request,
715+
conn: Annotated[connection, Depends(get_conn)],
716+
view_type: ViewType,
717+
subject: str,
718+
scopeuuid: str,
719+
):
720+
scopes = get_scopes()
721+
scope_name = scopes[scopeuuid]['name'] if scopeuuid in scopes else scopeuuid
722+
with conn.cursor() as cur:
723+
history = db_get_report_history(cur, subject, scopeuuid, limit=HISTORY_LIMIT)
724+
return render_view(
725+
VIEW_HISTORY, view_type, history=history, subject=subject, scope_name=scope_name,
726+
history_limit=HISTORY_LIMIT, base_url=settings.base_url, title=f'📜 Compliance history for {subject}',
727+
)
728+
729+
705730
@app.get("/{view_type}/table")
706731
async def get_table(
707732
request: Request,

compliance-monitor/sql.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def db_upgrade_schema(conn: connection, cur: cursor):
194194
# that way just in case we want to use another database at some point
195195
while True:
196196
current = db_get_schema_version(cur)
197-
if current >= SCHEMA_VERSIONS[-1]: # bail if version is too new (but hope it's compatible)
197+
if current is not None and current >= SCHEMA_VERSIONS[-1]: # bail if version is too new (but hope it's compatible)
198198
break
199199
if current is None:
200200
# this is an empty db, but it also used to be the case with v1
@@ -419,6 +419,25 @@ def db_get_recent_results2(cur: cursor, approved, limit, skip, max_age_days=None
419419
return [{col: val for col, val in zip(columns, row)} for row in cur.fetchall()]
420420

421421

422+
def db_get_report_history(cur: cursor, subject, scopeuuid, limit=500):
423+
"""list all past reports for a subject/scope, newest first, with pass/fail/abort counts"""
424+
cur.execute('''
425+
SELECT report.reportuuid, report.checked_at,
426+
COUNT(*) FILTER (WHERE result2.result = 1) AS pass_count,
427+
COUNT(*) FILTER (WHERE result2.result = -1) AS fail_count,
428+
COUNT(*) FILTER (WHERE result2.result = 0) AS abort_count
429+
FROM report
430+
JOIN result2 ON result2.reportid = report.reportid
431+
WHERE report.subject = %(subject)s
432+
AND result2.scopeuuid = %(scopeuuid)s
433+
GROUP BY report.reportuuid, report.checked_at
434+
ORDER BY report.checked_at DESC
435+
LIMIT %(limit)s;
436+
''', {"subject": subject, "scopeuuid": scopeuuid, "limit": limit})
437+
columns = ('reportuuid', 'checked_at', 'pass_count', 'fail_count', 'abort_count')
438+
return [{col: val for col, val in zip(columns, row)} for row in cur.fetchall()]
439+
440+
422441
def db_patch_approval2(cur: cursor, record):
423442
cur.execute('''
424443
UPDATE result2

compliance-monitor/templates/details.md.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Jump to
2222
## {{ subject }}: {{ scope_result.name }}
2323

2424
- [spec overview]({{ scope_url(scopeuuid) }})
25+
- [📜 compliance history]({{ history_url(subject, scopeuuid) }})
2526

2627
{% if not scope_result.relevant -%}
2728

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## 📜 Compliance history: {{ subject }} — {{ scope_name }}
2+
3+
{% if history %}
4+
| Date | PASS | FAIL | ABORT | Report |
5+
|---|---|---|---|---|
6+
{% for entry in history -%}
7+
| {{ entry.checked_at | short_isodate }} | {{ entry.pass_count }} | {{ entry.fail_count }} | {{ entry.abort_count }} | [view]({{ report_url(entry.reportuuid) }}) |
8+
{% endfor %}
9+
{% if history | length == history_limit %}
10+
_Showing the most recent {{ history_limit }} reports. Older entries are not displayed._
11+
{% endif %}
12+
{% else %}
13+
No history available.
14+
{% endif %}

compliance-monitor/templates/report.md.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- uuid: [{{ report.run.uuid }}]({{ report_url(report.run.uuid, download=True) }})
44
- subject: {{ report.subject }}
55
- scope: [{{ report.spec.name }}]({{ scope_url(report.spec.uuid) }})
6+
- [📜 compliance history]({{ history_url(report.subject, report.spec.uuid) }})
67
- checked at: {{ report.checked_at }}
78
- variable assignment: {% set comma = joiner(", ") %}{% for key, value in report.run.assignment.items() -%}{{comma()}}`{{ key }}`=`{{ value }}`{% endfor %}
89

0 commit comments

Comments
 (0)