Build Flask WAF log converter app

This commit is contained in:
Alfredo Di Stasio
2026-04-24 14:40:32 +02:00
parent f9579bd253
commit 355d61f11f
23 changed files with 1053 additions and 1 deletions

38
app/templates/base.html Normal file
View File

@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WAF Log Converter</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
>
</head>
<body class="bg-body-tertiary">
<main class="container py-5">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="mb-4">
<h1 class="display-6 fw-semibold">WAF Log Converter</h1>
<p class="text-secondary mb-0">
Upload a UTF-8 WAF log file and export a filtered report as readable text or CSV.
</p>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</div>
</main>
</body>
</html>

100
app/templates/index.html Normal file
View File

@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% set form = form or none %}
{% block content %}
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<form method="post" action="{{ url_for('main.convert') }}" enctype="multipart/form-data" novalidate>
<div class="mb-4">
<label for="log_file" class="form-label fw-semibold">Log file</label>
<input class="form-control" id="log_file" name="log_file" type="file" required>
<div class="form-text">Each line must contain one record using shell-like key/value tokens.</div>
</div>
<div class="row g-3">
<div class="col-md-3">
<label for="mode" class="form-label">Mode</label>
<select class="form-select" id="mode" name="mode">
<option value="vendor" {% if form and form.mode == "vendor" %}selected{% endif %}>Vendor</option>
<option value="full" {% if form and form.mode == "full" %}selected{% endif %}>Full</option>
</select>
</div>
<div class="col-md-3">
<label for="output_format" class="form-label">Format</label>
<select class="form-select" id="output_format" name="output_format">
<option value="text" {% if form and form.output_format == "text" %}selected{% endif %}>Text</option>
<option value="csv" {% if form and form.output_format == "csv" %}selected{% endif %}>CSV</option>
</select>
</div>
<div class="col-md-3">
<label for="sort_by" class="form-label">Sort by</label>
<select class="form-select" id="sort_by" name="sort_by">
<option value="datetime" {% if not form or form.sort_by == "datetime" %}selected{% endif %}>Datetime</option>
<option value="severity" {% if form and form.sort_by == "severity" %}selected{% endif %}>Severity</option>
</select>
</div>
<div class="col-md-3">
<label for="order" class="form-label">Order</label>
<select class="form-select" id="order" name="order">
<option value="asc" {% if not form or form.order == "asc" %}selected{% endif %}>Ascending</option>
<option value="desc" {% if form and form.order == "desc" %}selected{% endif %}>Descending</option>
</select>
</div>
</div>
<hr class="my-4">
<div class="row g-3">
<div class="col-md-6">
<label for="policy_cs" class="form-label">Policy filter, case-sensitive</label>
<input
class="form-control"
id="policy_cs"
name="policy_cs"
type="text"
value="{{ form.policy_cs if form else '' }}"
>
</div>
<div class="col-md-6">
<label for="policy_ci" class="form-label">Policy filter, case-insensitive</label>
<input
class="form-control"
id="policy_ci"
name="policy_ci"
type="text"
value="{{ form.policy_ci if form else '' }}"
>
</div>
<div class="col-md-6">
<label for="severity_cs" class="form-label">Severity filter, case-sensitive</label>
<input
class="form-control"
id="severity_cs"
name="severity_cs"
type="text"
value="{{ form.severity_cs if form else '' }}"
>
</div>
<div class="col-md-6">
<label for="severity_ci" class="form-label">Severity filter, case-insensitive</label>
<input
class="form-control"
id="severity_ci"
name="severity_ci"
type="text"
value="{{ form.severity_ci if form else '' }}"
>
</div>
</div>
<div class="alert alert-light border mt-4 mb-0" role="note">
Use only one policy filter and one severity filter at a time. Matching happens as a partial substring.
</div>
<div class="mt-4 d-flex gap-2">
<button class="btn btn-primary" type="submit">Convert log</button>
<button class="btn btn-outline-secondary" type="reset">Reset</button>
</div>
</form>
</div>
</div>
{% endblock %}

45
app/templates/result.html Normal file
View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block content %}
<div class="row g-4">
<div class="col-lg-4">
<div class="card shadow-sm border-0 h-100">
<div class="card-body">
<h2 class="h4">Result summary</h2>
<dl class="row mb-4">
<dt class="col-sm-5">Parsed records</dt>
<dd class="col-sm-7">{{ parsed_count }}</dd>
<dt class="col-sm-5">Output records</dt>
<dd class="col-sm-7">{{ filtered_count }}</dd>
<dt class="col-sm-5">Mode</dt>
<dd class="col-sm-7 text-capitalize">{{ mode }}</dd>
<dt class="col-sm-5">Format</dt>
<dd class="col-sm-7 text-uppercase">{{ output_format }}</dd>
<dt class="col-sm-5">Sort</dt>
<dd class="col-sm-7">{{ sort_by }} / {{ order }}</dd>
</dl>
<div class="d-grid gap-2">
<a class="btn btn-primary" href="{{ url_for('main.download', result_id=result_id) }}">
Download export
</a>
<a class="btn btn-outline-secondary" href="{{ url_for('main.index') }}">
Convert another file
</a>
</div>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h4 mb-0">Preview</h2>
<span class="badge text-bg-secondary">Showing up to {{ record_count if record_count < 5 else 5 }} records</span>
</div>
<pre class="bg-dark-subtle p-3 rounded small mb-0" style="white-space: pre-wrap;">{{ preview_text }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}