Add audit review export functionality in markdown and JSON formats
This commit is contained in:
Binary file not shown.
+133
@@ -185,6 +185,62 @@ def _append_audit_event_standalone(event_type, payload):
|
|||||||
client.close()
|
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):
|
def _parse_money_value(value):
|
||||||
"""Parse a user-facing money value into a float when possible."""
|
"""Parse a user-facing money value into a float when possible."""
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -5364,6 +5420,83 @@ def admin_audit_dashboard():
|
|||||||
client.close()
|
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'])
|
@app.route('/admin/reset_borrowing/<borrow_id>', methods=['POST'])
|
||||||
def admin_reset_borrowing(borrow_id):
|
def admin_reset_borrowing(borrow_id):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
<h1>Audit Dashboard</h1>
|
<h1>Audit Dashboard</h1>
|
||||||
<p>Integritätsstatus der Audit-Chain und letzte Audit-Ereignisse (max. 200 Einträge).</p>
|
<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="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="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background:#fff;">
|
||||||
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>
|
<div style="font-size:0.85rem; color:#64748b;">Chain Status</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user