Add audit review export functionality in markdown and JSON formats

This commit is contained in:
2026-04-10 21:22:32 +02:00
parent 0581b5c5e9
commit 444a1df1aa
3 changed files with 138 additions and 0 deletions
Binary file not shown.
+133
View File
@@ -185,6 +185,62 @@ def _append_audit_event_standalone(event_type, payload):
client.close()
def _build_audit_review_markdown(verify_result, event_counts, audit_rows, generated_at=None):
"""Build a detailed, downloadable markdown review for audit status and events."""
ts = generated_at or datetime.datetime.utcnow().isoformat() + 'Z'
lines = []
lines.append('# Audit Review Export')
lines.append('')
lines.append(f'- Generated at: {ts}')
lines.append(f"- Chain status: {'OK' if verify_result.get('ok') else 'ERROR'}")
lines.append(f"- Total entries: {verify_result.get('count', 0)}")
lines.append(f"- Last chain index: {verify_result.get('last_chain_index', 0)}")
lines.append(f"- Last hash: {verify_result.get('last_hash', '')}")
lines.append(f"- Mismatch count: {len(verify_result.get('mismatches', []) or [])}")
lines.append('')
lines.append('## Event Type Distribution')
lines.append('')
if event_counts:
for item in event_counts:
lines.append(f"- {item.get('event_type', 'unknown')}: {item.get('count', 0)}")
else:
lines.append('- No events available')
lines.append('')
mismatches = verify_result.get('mismatches', []) or []
lines.append('## Chain Mismatches')
lines.append('')
if mismatches:
for mismatch in mismatches:
lines.append(
f"- index={mismatch.get('chain_index')} | error={mismatch.get('error')} | expected={mismatch.get('expected')} | found={mismatch.get('found')}"
)
else:
lines.append('- None')
lines.append('')
lines.append('## Recent Events')
lines.append('')
for row in audit_rows:
lines.append(f"### Event #{row.get('chain_index')}")
lines.append(f"- Timestamp: {row.get('timestamp') or row.get('created_at')}")
lines.append(f"- Type: {row.get('event_type', '')}")
lines.append(f"- Actor: {row.get('actor', '')}")
lines.append(f"- Source: {row.get('source', '')}")
lines.append(f"- IP: {row.get('ip', '')}")
lines.append(f"- Entry hash: {row.get('entry_hash', '')}")
lines.append(f"- Prev hash: {row.get('prev_hash', '')}")
payload_json = json.dumps(row.get('payload', {}), ensure_ascii=False, sort_keys=True, indent=2, default=str)
lines.append('- Payload:')
lines.append('```json')
lines.append(payload_json)
lines.append('```')
lines.append('')
return '\n'.join(lines)
def _parse_money_value(value):
"""Parse a user-facing money value into a float when possible."""
if value is None:
@@ -5364,6 +5420,83 @@ def admin_audit_dashboard():
client.close()
@app.route('/admin/audit/export', methods=['GET'])
def admin_audit_export():
"""Export a detailed audit review in markdown or json format."""
if 'username' not in session or not us.check_admin(session['username']):
return jsonify({'ok': False, 'error': 'forbidden'}), 403
fmt = (request.args.get('format') or 'md').strip().lower()
try:
limit = int((request.args.get('limit') or '1000').strip())
except Exception:
limit = 1000
limit = max(1, min(limit, 5000))
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
al.ensure_audit_indexes(db)
verify_result = al.verify_audit_chain(db)
event_counts = list(
db['audit_log'].aggregate([
{'$group': {'_id': '$event_type', 'count': {'$sum': 1}}},
{'$project': {'_id': 0, 'event_type': {'$ifNull': ['$_id', 'unknown']}, 'count': 1}},
{'$sort': {'count': -1, 'event_type': 1}}
])
)
audit_rows = list(
db['audit_log'].find(
{},
{
'chain_index': 1,
'event_type': 1,
'actor': 1,
'source': 1,
'ip': 1,
'timestamp': 1,
'created_at': 1,
'entry_hash': 1,
'prev_hash': 1,
'payload': 1,
}
).sort('chain_index', -1).limit(limit)
)
generated_at = datetime.datetime.utcnow().isoformat() + 'Z'
if fmt == 'json':
export_payload = {
'generated_at': generated_at,
'verify_result': verify_result,
'event_counts': event_counts,
'events': audit_rows,
}
response = make_response(json.dumps(export_payload, ensure_ascii=False, indent=2, default=str))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename=audit-review-{datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S")}.json'
return response
markdown_content = _build_audit_review_markdown(
verify_result=verify_result,
event_counts=event_counts,
audit_rows=audit_rows,
generated_at=generated_at,
)
response = make_response(markdown_content)
response.headers['Content-Type'] = 'text/markdown; charset=utf-8'
response.headers['Content-Disposition'] = f'attachment; filename=audit-review-{datetime.datetime.utcnow().strftime("%Y%m%d-%H%M%S")}.md'
return response
except Exception as exc:
return jsonify({'ok': False, 'error': str(exc)}), 500
finally:
if client:
client.close()
@app.route('/admin/reset_borrowing/<borrow_id>', methods=['POST'])
def admin_reset_borrowing(borrow_id):
"""
+5
View File
@@ -5,6 +5,11 @@
<h1>Audit Dashboard</h1>
<p>Integritätsstatus der Audit-Chain und letzte Audit-Ereignisse (max. 200 Einträge).</p>
<div style="display:flex; gap:10px; flex-wrap:wrap; margin:10px 0 18px;">
<a class="btn btn-primary" href="{{ url_for('admin_audit_export', format='md') }}">Audit Review exportieren (Markdown)</a>
<a class="btn btn-outline-secondary" href="{{ url_for('admin_audit_export', format='json') }}">Audit Review exportieren (JSON)</a>
</div>
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>