On-demand analysis of past recordings

* rayhunter-daemon: API for triggering and reading analysis
* rayhunter-daemon: rename readonly mode to debug mode
* rayhunter-daemon: debug mode allows live-loading frontend files
* rayhunter-check: rework to handle directories
* rayhunter-check: better output
* CI: build rayhunter-check
This commit is contained in:
Will Greenberg
2024-08-15 18:18:19 -07:00
committed by Cooper Quintin
parent c59fb7c013
commit df84faa1f9
13 changed files with 636 additions and 134 deletions

View File

@@ -1,16 +1,99 @@
const STATUS_RUNNING = 'running';
const STATUS_QUEUED = 'queued';
const STATUS_NEEDS_UPDATE = 'needs-update';
const STATUS_COMPLETE = 'complete';
async function populateDivs() {
const systemStats = await getSystemStats();
const systemStatsDiv = document.getElementById('system-stats');
systemStatsDiv.innerHTML = JSON.stringify(systemStats, null, 2);
const analysisReport = await getAnalysisReport();
const analysisReportDiv = document.getElementById('analysis-report');
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
try {
const analysisReport = await getAnalysisReport('live');
analysisReportDiv.innerHTML = JSON.stringify(analysisReport, null, 2);
} catch (e) {
analysisReportDiv.innerHTML = e.toString();
}
const qmdlManifest = await getQmdlManifest();
await updateAnalysisStatus(qmdlManifest);
await updateAnalysisResults(qmdlManifest);
updateQmdlManifestTable(qmdlManifest);
}
function setStatus(qmdlManifest, name, status) {
// ignore qmdlManifest.current_entry, it's always running
for (const entry of qmdlManifest.entries) {
if (entry.name === name) {
entry['status'] = status;
return;
}
}
}
async function updateAnalysisStatus(qmdlManifest) {
const status = JSON.parse(await req('GET', '/api/analysis'));
if (status.running) {
setStatus(qmdlManifest, status.running, STATUS_RUNNING);
}
for (const queued in status.queued) {
setStatus(qmdlManifest, queued, STATUS_QUEUED);
}
}
function parseNewlineDelimitedJSON(inputStr) {
const lines = inputStr.split('\n');
const result = [];
let currentLine = '';
while (lines.length > 0) {
currentLine += lines.shift();
try {
const entry = JSON.parse(currentLine);
result.push(entry);
// if this chunk wasn't valid JSON, there was an escaped newline in the
// JSON line, so simply continue to the next one
} catch (e) {}
}
return result;
}
async function updateEntryAnalysisResult(entry) {
entry.analysis = {
warnings: [],
};
const report = parseNewlineDelimitedJSON(await req('GET', `/api/analysis-report/${entry.name}`));
for (const row of report) {
if (row["analysis"]) {
const timestamp = new Date(row["timestamp"]);
const analysis = row["analysis"];
for (const warning of analysis) {
entry.warnings.push({
timestamp,
warning,
})
}
}
}
if (entry.analysis.warnings.length === 0) {
entry.analysis_result = `0 warnings!`;
} else {
entry.analysis_result = `!!! ${entry.analysis.warnings.length} warnings !!!`;
}
}
async function updateAnalysisResults(qmdlManifest) {
if (qmdlManifest.current_entry) {
await updateEntryAnalysisResult(qmdlManifest.current_entry);
}
for (const entry of qmdlManifest.entries) {
if (entry.status === STATUS_NEEDS_UPDATE) {
await updateEntryAnalysisResult(entry);
entry.status = STATUS_COMPLETE;
}
}
}
function updateQmdlManifestTable(manifest) {
const table = document.getElementById('qmdl-manifest-table');
const numRows = table.rows.length;
@@ -18,43 +101,52 @@ function updateQmdlManifestTable(manifest) {
table.deleteRow(1);
}
if (manifest.current_entry) {
const row = createEntryRow(manifest.current_entry);
const row = createEntryRow(manifest.current_entry, true);
row.classList.add('current');
table.appendChild(row)
}
for (let entry of manifest.entries) {
table.appendChild(createEntryRow(entry));
table.appendChild(createEntryRow(entry), false);
}
}
function createEntryRow(entry) {
function createLink(uri, text) {
const link = document.createElement('a');
link.href = uri;
link.innerText = text;
return link;
}
function createEntryRow(entry, isCurrent) {
const row = document.createElement('tr');
const name = document.createElement('th');
name.scope = 'row';
name.innerText = entry.name;
row.appendChild(name);
for (const key of ['start_time', 'last_message_time', 'qmdl_size_bytes']) {
const td = document.createElement('td');
td.innerText = entry[key];
row.appendChild(td);
}
const pcap_td = document.createElement('td');
const pcap_link = document.createElement('a');
pcap_link.href = `/api/pcap/${entry.name}`;
pcap_link.innerText = 'pcap';
pcap_td.appendChild(pcap_link);
row.appendChild(pcap_td);
const qmdl_td = document.createElement('td');
const qmdl_link = document.createElement('a');
qmdl_link.href = `/api/qmdl/${entry.name}`;
qmdl_link.innerText = 'qmdl';
qmdl_td.appendChild(qmdl_link);
row.appendChild(qmdl_td);
const pcapTd = document.createElement('td');
pcapTd.appendChild(createLink(`/api/pcap/${entry.name}`, 'pcap'));
row.appendChild(pcapTd);
const qmdlTd = document.createElement('td');
qmdlTd.appendChild(createLink(`/api/qmdl/${entry.name}`, 'qmdl'));
row.appendChild(qmdlTd);
const analysisResult = document.createElement('td');
analysisResult.innerText = entry.analysis_result;
row.appendChild(analysisResult);
return row;
}
async function getAnalysisReport() {
const rows = await req('GET', '/api/analysis-report');
async function getAnalysisReport(name) {
const rows = await req('GET', `/api/analysis-report/${name}`);
return rows.split('\n')
.filter(row => row.length > 0)
.map(row => JSON.parse(row));
@@ -67,6 +159,8 @@ async function getSystemStats() {
async function getQmdlManifest() {
const manifest = JSON.parse(await req('GET', '/api/qmdl-manifest'));
if (manifest.current_entry) {
manifest.current_entry.status = STATUS_NEEDS_UPDATE;
manifest.current_entry.analysis_result = 'Waiting...';
manifest.current_entry.start_time = new Date(manifest.current_entry.start_time);
if (manifest.current_entry.last_message_time === undefined) {
manifest.current_entry.last_message_time = "N/A";
@@ -75,6 +169,8 @@ async function getQmdlManifest() {
}
}
for (entry of manifest.entries) {
entry.status = STATUS_NEEDS_UPDATE;
entry.analysis_result = 'Waiting...';
entry.start_time = new Date(entry.start_time);
entry.last_message_time = new Date(entry.last_message_time);
}