Replace monolithic CLI and web server with route-based Flask API. New routes: api_commands, api_build, api_can, api_monitor, api_ota, api_tunnel. Add honeypot security dashboard with real-time SSE, MITRE ATT&CK mapping, kill chain analysis. New TUI with commander/help modules. Add session management, tunnel proxy core, CAN bus data store. Docker support.
226 lines
12 KiB
HTML
226 lines
12 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}CAN Bus - ESPILON{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page" x-data="canbusApp()" x-init="init()">
|
|
<div class="split-v" style="flex:1;">
|
|
|
|
<!-- Top: Frame table -->
|
|
<div class="panel flex-1">
|
|
<div class="toolbar">
|
|
<span class="toolbar-label">Frames</span>
|
|
<select class="select" x-model="selectedDevice" @change="refreshFrames()">
|
|
<option value="">all devices</option>
|
|
<template x-for="d in $store.app.connectedDevices()" :key="d.id">
|
|
<option :value="d.id" x-text="d.id"></option>
|
|
</template>
|
|
</select>
|
|
<input type="text" class="input" x-model="filterCanId" placeholder="CAN ID (hex)" style="width:120px;" @input="refreshFrames()">
|
|
<div class="toolbar-sep"></div>
|
|
<label class="checkbox-label"><input type="checkbox" x-model="autoRefresh"> Auto</label>
|
|
<button class="btn btn-sm" @click="refreshFrames()">Refresh</button>
|
|
<button class="btn btn-sm" @click="exportCsv()">Export CSV</button>
|
|
<div class="toolbar-sep"></div>
|
|
<span class="text-xs text-muted" x-text="rows.length + ' frames'"></span>
|
|
</div>
|
|
<div class="panel-body">
|
|
<table class="dt">
|
|
<thead>
|
|
<tr>
|
|
<th @click="toggleSort('received_at')" :class="sortClass('received_at')">Time</th>
|
|
<th @click="toggleSort('device_id')" :class="sortClass('device_id')">Device</th>
|
|
<th @click="toggleSort('can_id')" :class="sortClass('can_id')">CAN ID</th>
|
|
<th @click="toggleSort('dlc')" :class="sortClass('dlc')" class="col-center">DLC</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="f in pagedRows" :key="f.received_at + f.can_id + Math.random()">
|
|
<tr>
|
|
<td x-text="formatTimestamp(f.received_at)"></td>
|
|
<td x-text="f.device_id"></td>
|
|
<td x-text="'0x' + f.can_id"></td>
|
|
<td class="col-center" x-text="f.dlc"></td>
|
|
<td class="col-wrap" x-text="f.data_hex"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
<template x-if="rows.length === 0 && !loading">
|
|
<div class="dt-empty">No CAN frames captured</div>
|
|
</template>
|
|
</div>
|
|
<div class="panel-footer" x-show="totalPages > 1">
|
|
Page <span x-text="page + 1"></span>/<span x-text="totalPages"></span>
|
|
<button class="btn btn-sm" @click="page = Math.max(0, page-1)" :disabled="page===0">«</button>
|
|
<button class="btn btn-sm" @click="page = Math.min(totalPages-1, page+1)" :disabled="page>=totalPages-1">»</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="resizer resizer-h"></div>
|
|
|
|
<!-- Bottom: Controls -->
|
|
<div class="panel" style="flex:0 0 auto;max-height:40%;">
|
|
<div class="subtabs">
|
|
<div class="subtab" :class="bottomTab==='controls'?'active':''" @click="bottomTab='controls'">Controls</div>
|
|
<div class="subtab" :class="bottomTab==='uds'?'active':''" @click="bottomTab='uds'">UDS / OBD</div>
|
|
<div class="subtab" :class="bottomTab==='stats'?'active':''" @click="bottomTab='stats'">Stats</div>
|
|
</div>
|
|
<div class="panel-body panel-body-pad" style="overflow-y:auto;">
|
|
|
|
<!-- Controls Tab -->
|
|
<div x-show="bottomTab==='controls'">
|
|
<div class="form-row">
|
|
<span class="form-label">Device</span>
|
|
<select class="select" x-model="cmdDevice">
|
|
<template x-for="d in $store.app.connectedDevices()" :key="d.id">
|
|
<option :value="d.id" x-text="d.id"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div class="flex gap-2" style="flex-wrap:wrap;margin-top:8px;">
|
|
<button class="btn btn-sm btn-success" @click="cmd('can_start')">Start</button>
|
|
<button class="btn btn-sm btn-danger" @click="cmd('can_stop')">Stop</button>
|
|
<button class="btn btn-sm" @click="cmd('can_status')">Status</button>
|
|
<button class="btn btn-sm" @click="cmd('can_sniff', [sniffDuration])">Sniff</button>
|
|
<input type="number" class="input" x-model="sniffDuration" style="width:60px;" placeholder="10s">
|
|
<button class="btn btn-sm" @click="cmd('can_record', [recordDuration])">Record</button>
|
|
<input type="number" class="input" x-model="recordDuration" style="width:60px;" placeholder="10s">
|
|
<button class="btn btn-sm" @click="cmd('can_dump')">Dump</button>
|
|
<button class="btn btn-sm" @click="cmd('can_replay', [replaySpeed])">Replay</button>
|
|
<input type="number" class="input" x-model="replaySpeed" style="width:60px;" placeholder="100%">
|
|
</div>
|
|
<div class="form-row" style="margin-top:8px;">
|
|
<span class="form-label">Send</span>
|
|
<input type="text" class="input" x-model="sendId" placeholder="ID (hex)" style="width:90px;">
|
|
<input type="text" class="input flex-1" x-model="sendData" placeholder="Data (hex)">
|
|
<button class="btn btn-sm btn-primary" @click="cmd('can_send', [sendId, sendData])">TX</button>
|
|
</div>
|
|
<div class="form-row" style="margin-top:4px;">
|
|
<span class="form-label">Filter</span>
|
|
<input type="text" class="input" x-model="filterId" placeholder="CAN ID (hex)" style="width:90px;">
|
|
<button class="btn btn-sm" @click="cmd('can_filter_add', [filterId])">Add</button>
|
|
<button class="btn btn-sm" @click="cmd('can_filter_del', [filterId])">Del</button>
|
|
<button class="btn btn-sm" @click="cmd('can_filter_list')">List</button>
|
|
<button class="btn btn-sm btn-danger" @click="cmd('can_filter_clear')">Clear</button>
|
|
</div>
|
|
<!-- Command output -->
|
|
<template x-if="cmdOutput.length > 0">
|
|
<pre class="build-log" style="margin-top:8px;max-height:120px;" x-text="cmdOutput.join('\n')"></pre>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- UDS/OBD Tab -->
|
|
<div x-show="bottomTab==='uds'">
|
|
<div class="flex gap-2" style="flex-wrap:wrap;">
|
|
<button class="btn btn-sm" @click="cmd('can_scan_ecu')">Scan ECUs</button>
|
|
<button class="btn btn-sm" @click="cmd('can_obd_vin')">Read VIN</button>
|
|
<button class="btn btn-sm" @click="cmd('can_obd_dtc')">Read DTCs</button>
|
|
<button class="btn btn-sm" @click="cmd('can_obd_supported')">Supported PIDs</button>
|
|
</div>
|
|
<div class="form-row" style="margin-top:8px;">
|
|
<span class="form-label">UDS Raw</span>
|
|
<input type="text" class="input" x-model="udsTxId" placeholder="TX ID" style="width:80px;">
|
|
<input type="text" class="input" x-model="udsSvc" placeholder="SVC (hex)" style="width:80px;">
|
|
<input type="text" class="input flex-1" x-model="udsData" placeholder="Data (hex)">
|
|
<button class="btn btn-sm btn-primary" @click="cmd('can_uds', [udsTxId, udsSvc, udsData].filter(Boolean))">Send</button>
|
|
</div>
|
|
<div class="form-row" style="margin-top:4px;">
|
|
<span class="form-label">OBD PID</span>
|
|
<input type="text" class="input" x-model="obdPid" placeholder="PID (hex)" style="width:80px;">
|
|
<button class="btn btn-sm" @click="cmd('can_obd', [obdPid])">Query</button>
|
|
<button class="btn btn-sm" @click="cmd('can_obd_monitor', [obdPid, obdInterval])">Monitor</button>
|
|
<input type="number" class="input" x-model="obdInterval" style="width:70px;" placeholder="1000ms">
|
|
<button class="btn btn-sm btn-danger" @click="cmd('can_obd_monitor_stop')">Stop</button>
|
|
</div>
|
|
<template x-if="cmdOutput.length > 0">
|
|
<pre class="build-log" style="margin-top:8px;max-height:120px;" x-text="cmdOutput.join('\n')"></pre>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Stats Tab -->
|
|
<div x-show="bottomTab==='stats'">
|
|
<template x-if="Object.keys(stats).length > 0">
|
|
<div class="kv">
|
|
<template x-for="(val, key) in stats" :key="key">
|
|
<div class="kv-row"><span class="kv-key" x-text="key"></span><span class="kv-val" x-text="val"></span></div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
<template x-if="Object.keys(stats).length === 0">
|
|
<div class="text-muted text-xs">No stats yet</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function canbusApp() {
|
|
return {
|
|
...dataTable({ defaultSort: 'received_at', defaultDir: 'desc', pageSize: 200, extract: d => d.frames || [] }),
|
|
...commander(),
|
|
selectedDevice: '',
|
|
filterCanId: '',
|
|
autoRefresh: true,
|
|
stats: {},
|
|
bottomTab: 'controls',
|
|
cmdDevice: '',
|
|
cmdOutput: [],
|
|
sniffDuration: '10', recordDuration: '10', replaySpeed: '100',
|
|
sendId: '', sendData: '', filterId: '',
|
|
udsTxId: '', udsSvc: '', udsData: '', obdPid: '', obdInterval: '1000',
|
|
|
|
init() {
|
|
this.refreshFrames();
|
|
this.refreshStats();
|
|
setInterval(() => { if (this.autoRefresh) this.refreshFrames(); }, 1000);
|
|
setInterval(() => this.refreshStats(), 5000);
|
|
},
|
|
|
|
async refreshFrames() {
|
|
let url = '/api/can/frames?limit=500';
|
|
if (this.selectedDevice) url += '&device_id=' + this.selectedDevice;
|
|
if (this.filterCanId) url += '&can_id=' + this.filterCanId;
|
|
await this.refresh(url);
|
|
},
|
|
|
|
async refreshStats() {
|
|
try { const r = await fetch('/api/can/stats'); this.stats = await r.json(); } catch(e) {}
|
|
},
|
|
|
|
exportCsv() { window.open('/api/can/frames/export', '_blank'); },
|
|
|
|
async cmd(command, argv) {
|
|
const dev = this.cmdDevice || (this.$store.app.connectedDevices()[0] || {}).id;
|
|
if (!dev) { toast('No device selected', 'error'); return; }
|
|
argv = (argv || []).map(String).filter(Boolean);
|
|
this.cmdOutput = ['Sending ' + command + '...'];
|
|
try {
|
|
const data = await this.sendCommand([dev], command, argv);
|
|
const r = (data.results || [])[0];
|
|
if (r && r.status === 'ok' && r.request_id) {
|
|
// Poll for result
|
|
let attempts = 0;
|
|
const iv = setInterval(async () => {
|
|
attempts++;
|
|
const res = await fetch('/api/commands/' + encodeURIComponent(r.request_id));
|
|
const d = await res.json();
|
|
if (d.output) this.cmdOutput = d.output;
|
|
if (d.status === 'completed' || d.status === 'error' || attempts >= 30) clearInterval(iv);
|
|
}, 500);
|
|
} else if (r) {
|
|
this.cmdOutput = [r.message || 'Error'];
|
|
}
|
|
} catch (e) {
|
|
this.cmdOutput = ['Error: ' + e.message];
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|