This commit is contained in:
Will Greenberg
2025-04-08 15:12:41 -07:00
parent 057c9acb40
commit cf2d406d88
7 changed files with 121 additions and 37 deletions

View File

@@ -1,13 +1,7 @@
import { parse_ndjson, type NewlineDeliminatedJson } from "./ndjson";
import { req } from "./utils";
export type AnalysisReport =
| LoadingReport
| FinishedReport;
export type LoadingReport = {};
export type FinishedReport = {
export type AnalysisReport = {
metadata: ReportMetadata;
rows: AnalysisRow[];
};
@@ -54,7 +48,7 @@ export type InformationalEvent = {
message: string;
};
export function parse_finished_report(report_json: NewlineDeliminatedJson): FinishedReport {
export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport {
const metadata: ReportMetadata = report_json[0]; // this can be cast directly
const rows: AnalysisRow[] = report_json.slice(1).map((row_json: any) => {
const analysis: PacketAnalysis[] = row_json.analysis.map((analysis_json: any) => {
@@ -93,7 +87,7 @@ export function parse_finished_report(report_json: NewlineDeliminatedJson): Fini
};
}
export async function get_report(name: string): Promise<FinishedReport> {
export async function get_report(name: string): Promise<AnalysisReport> {
const report_json = parse_ndjson(await req('GET', `/api/analysis-report/${name}`));
return parse_finished_report(report_json);
}

View File

@@ -1,9 +1,14 @@
import { get_report, type AnalysisReport } from "./analysis";
import type { Manifest, ManifestEntry } from "./manifest";
import { req } from "./utils";
export enum AnalysisStatus {
// rayhunter is currently analyzing this entry (note that this is distinct
// from the currently-recording entry)
Running,
// this entry is queued to be analyzed
Queued,
// analysis is finished, and the new report can be accessed
Finished,
}
@@ -19,27 +24,40 @@ export type AnalysisResult = {
};
export class AnalysisManager {
public analysis_status: Map<string, AnalysisStatus> = new Map();
public status: Map<string, AnalysisStatus> = new Map();
public reports: Map<string, AnalysisReport | string> = new Map();
public async run_analysis(name: string) {
await req('POST', `/api/analysis/${name}`);
this.analysis_status.set(name, AnalysisStatus.Queued);
this.status.set(name, AnalysisStatus.Queued);
this.reports.delete(name);
}
public async update() {
this.analysis_status.clear();
const status: AnalysisStatusJson = JSON.parse(await req('GET', '/api/analysis'));
if (status.running) {
this.analysis_status.set(status.running, AnalysisStatus.Running);
this.status.set(status.running, AnalysisStatus.Running);
}
for (const entry of status.queued) {
this.analysis_status.set(entry, AnalysisStatus.Queued);
this.status.set(entry, AnalysisStatus.Queued);
}
for (const entry of status.finished) {
this.analysis_status.set(entry, AnalysisStatus.Finished);
// if entry was already finished, nothing to do
if (this.status.get(entry) === AnalysisStatus.Finished) {
continue;
}
this.status.set(entry, AnalysisStatus.Finished);
// fetch the analysis report
this.reports.delete(entry);
get_report(entry).then(report => {
this.reports.set(entry, report);
}).catch(err => {
this.reports.set(entry, `Failed to get analysis: ${err}`);
});
}
}
}

View File

@@ -0,0 +1,40 @@
<script lang="ts">
import { AnalysisStatus } from "$lib/analysisManager";
import { EventType } from "$lib/analysis";
import type { ManifestEntry } from "$lib/manifest";
let { entry }: {
entry: ManifestEntry,
} = $props();
let summary = $state('Loading...');
if (entry.analysis_status === AnalysisStatus.Queued) {
summary = 'Queued...';
} else if (entry.analysis_status === AnalysisStatus.Running) {
summary = 'Running...';
} else if (entry.analysis_status === AnalysisStatus.Finished) {
if (entry.analysis_report === undefined) {
summary = 'Loading...';
} else if (typeof(entry.analysis_report) === 'string') {
summary = entry.analysis_report;
} else {
let num_warnings = 0;
for (let row of entry.analysis_report.rows) {
for (let analysis of row.analysis) {
for (let event of analysis.events) {
if (event.type === EventType.Warning) {
num_warnings += 1;
}
}
}
}
summary = `${num_warnings} warnings`;
}
}
</script>
<p>
{summary}
</p>
<style>
</style>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { ManifestEntry } from "$lib/manifest";
import DownloadLink from '$lib/components/DownloadLink.svelte';
import AnalysisStatus from "./AnalysisStatus.svelte";
let { entry, current }: {
entry: ManifestEntry;
current: boolean;
@@ -16,7 +17,7 @@
<td>{entry.qmdl_size_bytes}</td>
<td><DownloadLink url={entry.getPcapUrl()} text="pcap" /></td>
<td><DownloadLink url={entry.getQmdlUrl()} text="qmdl" /></td>
<td>N/A</td>
<td><AnalysisStatus entry={entry} /></td>
</tr>
<style>

View File

@@ -1,3 +1,6 @@
import { get_report, type AnalysisReport } from "./analysis";
import { AnalysisStatus, type AnalysisManager } from "./analysisManager";
interface JsonManifest {
entries: JsonManifestEntry[];
current_entry: JsonManifestEntry | null;
@@ -26,14 +29,35 @@ export class Manifest {
// sort entries in reverse chronological order
this.entries.reverse();
}
async set_analysis_status(manager: AnalysisManager) {
for (let entry of this.entries) {
entry.analysis_status = manager.status.get(entry.name);
entry.analysis_report = manager.reports.get(entry.name);
}
if (this.current_entry) {
try {
this.current_entry.analysis_report = await get_report(this.current_entry.name);
} catch(err) {
this.current_entry.analysis_report = `Err: failed to get analysis report: ${err}`;
}
// the current entry should always be considered "finished", as its
// analysis report is always available
this.current_entry.analysis_status = AnalysisStatus.Finished;
}
}
}
export class ManifestEntry {
public name: string;
public start_time: Date;
public last_message_time: Date | undefined;
public last_message_time: Date | undefined = undefined;
public qmdl_size_bytes: number;
public analysis_size_bytes: number;
public analysis_status: AnalysisStatus | undefined = undefined;
public analysis_report: AnalysisReport | string | undefined = undefined;
constructor(json: JsonManifestEntry) {
this.name = json.name;

View File

@@ -1,11 +1,15 @@
export type NewlineDeliminatedJson = any[];
export function parse_ndjson(input: string): NewlineDeliminatedJson {
console.log(input)
const lines = input.split('\n');
const result = [];
let current_line = '';
while (lines.length > 0) {
current_line += lines.shift();
if (current_line.length === 0) {
continue;
}
try {
const entry = JSON.parse(current_line);
result.push(entry);
@@ -16,7 +20,7 @@ export function parse_ndjson(input: string): NewlineDeliminatedJson {
// however, if we've reached the end of the input, that means we
// were given invalid nd-json
if (lines.length === 0) {
throw new Error(`unable to parse invalid nd-json: ${e}`);
throw new Error(`unable to parse invalid nd-json: ${e}, "${current_line}"`);
}
}
}