Add Chart.js line/bar charts for attack trends (attempts over time, hourly pattern), an SVG world map choropleth colored by attack origin country, and a collapsible filter form (date range, IP, country, username) that narrows both charts and top-N tables. New store methods: GetAttemptsOverTime, GetHourlyPattern, GetCountryStats, and filtered variants of dashboard stats/top-N queries. New JSON API endpoints at /api/charts/* and an htmx fragment at /fragments/dashboard-content for filtered table updates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
175 lines
5.7 KiB
HTML
175 lines
5.7 KiB
HTML
{{define "content"}}
|
|
<section id="stats-section" hx-get="/fragments/stats" hx-trigger="every 30s" hx-swap="innerHTML">
|
|
{{template "stats" .Stats}}
|
|
</section>
|
|
|
|
<details>
|
|
<summary>Filters</summary>
|
|
<form id="filter-form">
|
|
<div class="grid">
|
|
<label>Since <input type="date" name="since"></label>
|
|
<label>Until <input type="date" name="until"></label>
|
|
<label>IP <input type="text" name="ip" placeholder="10.0.0.1"></label>
|
|
<label>Country <input type="text" name="country" placeholder="CN" maxlength="2"></label>
|
|
<label>Username <input type="text" name="username" placeholder="root"></label>
|
|
</div>
|
|
<button type="submit">Apply</button>
|
|
<button type="button" class="secondary" onclick="clearFilters()">Clear</button>
|
|
</form>
|
|
</details>
|
|
|
|
<section>
|
|
<h3>Attack Trends</h3>
|
|
<div class="grid">
|
|
<article>
|
|
<header>Attempts Over Time</header>
|
|
<canvas id="chart-attempts"></canvas>
|
|
</article>
|
|
<article>
|
|
<header>Hourly Pattern (UTC)</header>
|
|
<canvas id="chart-hourly"></canvas>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h3>Attack Origins</h3>
|
|
<article>
|
|
<div id="world-map"></div>
|
|
</article>
|
|
</section>
|
|
|
|
<div id="dashboard-content">
|
|
{{template "dashboard_content" .}}
|
|
</div>
|
|
|
|
<section>
|
|
<h3>Active Sessions</h3>
|
|
<div id="active-sessions" hx-get="/fragments/active-sessions" hx-trigger="every 10s" hx-swap="innerHTML">
|
|
{{template "active_sessions" .ActiveSessions}}
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h3>Recent Sessions</h3>
|
|
<table id="recent-sessions-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>IP</th>
|
|
<th>Country</th>
|
|
<th>Username</th>
|
|
<th>Type</th>
|
|
<th>Score</th>
|
|
<th>Connected</th>
|
|
<th>Disconnected</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .RecentSessions}}
|
|
<tr>
|
|
<td><a href="/sessions/{{.ID}}"><code>{{truncateID .ID}}</code></a>{{if gt .EventCount 0}} <mark>replay</mark>{{end}}</td>
|
|
<td>{{.IP}}</td>
|
|
<td>{{.Country}}</td>
|
|
<td>{{.Username}}</td>
|
|
<td>{{if .ExecCommand}}<mark>exec</mark>{{else}}{{.ShellName}}{{end}}</td>
|
|
<td>{{if .HumanScore}}{{if gt (derefFloat .HumanScore) 0.6}}<mark>{{formatScore .HumanScore}}</mark>{{else}}{{formatScore .HumanScore}}{{end}}{{else}}-{{end}}</td>
|
|
<td>{{formatTime .ConnectedAt}}</td>
|
|
<td>{{if .DisconnectedAt}}{{formatTime (derefTime .DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
|
</tr>
|
|
{{else}}
|
|
<tr><td colspan="8">No sessions</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{define "scripts"}}
|
|
<script src="/static/chart.min.js"></script>
|
|
<script src="/static/dashboard.js"></script>
|
|
{{end}}
|
|
|
|
{{define "dashboard_content"}}
|
|
<section>
|
|
<h3>Top Credentials & IPs</h3>
|
|
<div class="top-grid">
|
|
<article>
|
|
<header>Top Usernames</header>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Username</th><th>Attempts</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .TopUsernames}}
|
|
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="2">No data</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
<article>
|
|
<header>Top Passwords</header>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Password</th><th>Attempts</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .TopPasswords}}
|
|
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="2">No data</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
<article>
|
|
<header>Top IPs</header>
|
|
<table>
|
|
<thead>
|
|
<tr><th>IP</th><th>Country</th><th>Attempts</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .TopIPs}}
|
|
<tr><td>{{.Value}}</td><td>{{.Country}}</td><td>{{.Count}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="3">No data</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
<article>
|
|
<header>Top Countries</header>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Country</th><th>Attempts</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .TopCountries}}
|
|
<tr><td>{{.Value}}</td><td>{{.Count}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="2">No data</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
<article>
|
|
<header>Top Exec Commands</header>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Command</th><th>Count</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .TopExecCommands}}
|
|
<tr><td><code>{{truncateCommand .Value}}</code></td><td>{{.Count}}</td></tr>
|
|
{{else}}
|
|
<tr><td colspan="2">No data</td></tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
{{end}}
|