diff --git a/daemon/web/src/lib/analysis.spec.svelte.ts b/daemon/web/src/lib/analysis.spec.svelte.ts deleted file mode 100644 index 225fb28..0000000 --- a/daemon/web/src/lib/analysis.spec.svelte.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { EventType, parse_finished_report, Severity } from './analysis.svelte'; -import { type NewlineDeliminatedJson } from './ndjson'; - -const SAMPLE_REPORT_NDJSON: NewlineDeliminatedJson = [ - { - analyzers: [ - { - name: 'LTE SIB 6/7 Downgrade', - description: - 'Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.', - }, - { - name: 'IMSI Provided', - description: "Tests whether the UE's IMSI was ever provided to the cell", - }, - { - name: 'Null Cipher', - description: 'Tests whether the cell suggests using a null cipher (EEA0)', - }, - { - name: 'Example Analyzer', - description: - 'Always returns true, if you are seeing this you are either a developer or you are about to have problems.', - }, - ], - }, - { - timestamp: '2024-10-08T13:25:43.011689003-07:00', - skipped_message_reasons: [ - 'DecodingError(UperDecodeError(Error { cause: BufferTooShort, msg: "PerCodec:DecodeError:Requested Bits to decode 3, Remaining bits 1", context: [] }))', - ], - analysis: [], - }, - { - timestamp: '2024-10-08T13:25:43.480872496-07:00', - skipped_message_reasons: [], - analysis: [ - { - timestamp: '2024-08-19T03:33:54.318Z', - events: [ - null, - null, - null, - { - event_type: { type: 'QualitativeWarning', severity: 'Low' }, - message: 'TMSI was provided to cell', - }, - ], - }, - ], - }, -]; - -describe('analysis report parsing', () => { - it('parses the example analysis', () => { - const report = parse_finished_report(SAMPLE_REPORT_NDJSON); - expect(report.metadata.analyzers).toEqual([ - { - name: 'LTE SIB 6/7 Downgrade', - description: - 'Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.', - }, - { - name: 'IMSI Provided', - description: "Tests whether the UE's IMSI was ever provided to the cell", - }, - { - name: 'Null Cipher', - description: 'Tests whether the cell suggests using a null cipher (EEA0)', - }, - { - name: 'Example Analyzer', - description: - 'Always returns true, if you are seeing this you are either a developer or you are about to have problems.', - }, - ]); - expect(report.rows).toHaveLength(2); - expect(report.rows[0].skipped_message_reasons).toHaveLength(1); - expect(report.rows[0].analysis).toHaveLength(0); - expect(report.rows[1].skipped_message_reasons).toHaveLength(0); - expect(report.rows[1].analysis).toHaveLength(1); - expect(report.rows[1].analysis[0].events).toHaveLength(1); - const event = report.rows[1].analysis[0].events[0]; - if (event.type === EventType.Warning) { - expect(event.severity).toEqual(Severity.Low); - } else { - throw 'wrong event type'; - } - }); -}); diff --git a/daemon/web/src/lib/analysis.svelte.spec.ts b/daemon/web/src/lib/analysis.svelte.spec.ts new file mode 100644 index 0000000..fdd1364 --- /dev/null +++ b/daemon/web/src/lib/analysis.svelte.spec.ts @@ -0,0 +1,142 @@ +import { describe, it, expect } from 'vitest'; +import { AnalysisRowType, EventType, parse_finished_report, Severity } from './analysis.svelte'; +import { type NewlineDeliminatedJson } from './ndjson'; + +const SAMPLE_V1_REPORT_NDJSON: NewlineDeliminatedJson = [ + { + analyzers: [ + { + name: "Analyzer 1", + description: "A first analyzer", + }, + { + name: "Analyzer 2", + description: "A second analyzer", + }, + ], + }, + { + timestamp: '2024-10-08T13:25:43.011689003-07:00', + skipped_message_reasons: [ + 'The reason why the message was skipped', + ], + analysis: [], + }, + { + timestamp: '2024-10-08T13:25:43.480872496-07:00', + skipped_message_reasons: [], + analysis: [ + { + timestamp: '2024-08-19T03:33:54.318Z', + events: [ + null, + { + event_type: { type: 'QualitativeWarning', severity: 'Low' }, + message: 'Something nasty happened', + }, + ], + }, + ], + }, +]; + +const SAMPLE_V2_REPORT_NDJSON: NewlineDeliminatedJson = [ + { + analyzers: [ + { + name: "Analyzer 1", + description: "A first analyzer", + version: 2, + }, + { + name: "Analyzer 2", + description: "A second analyzer", + version: 2, + }, + ], + report_version: 2, + }, + { + skipped_message_reason: 'The reason why the message was skipped', + }, + { + packet_timestamp: '2024-08-19T03:33:54.318Z', + events: [ + null, + { + event_type: { type: 'QualitativeWarning', severity: 'Low' }, + message: 'Something nasty happened', + }, + ], + }, +]; + +describe('analysis report parsing', () => { + it('parses v1 example analysis', () => { + const report = parse_finished_report(SAMPLE_V1_REPORT_NDJSON); + expect(report.metadata.report_version).toEqual(1); + expect(report.metadata.analyzers).toEqual([ + { + name: 'Analyzer 1', + description: "A first analyzer", + version: 1, + }, + { + name: 'Analyzer 2', + description: "A second analyzer", + version: 1, + }, + ]); + expect(report.rows).toHaveLength(2); + expect(report.rows[0].type).toBe(AnalysisRowType.Skipped); + if (report.rows[1].type === AnalysisRowType.Analysis) { + const row = report.rows[1]; + expect(row.events).toHaveLength(2); + expect(row.events[0]).toBeNull(); + const event = row.events[1]; + const expected_timestamp = new Date('2024-08-19T03:33:54.318Z'); + expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime()) + if (event !== null && event.type === EventType.Warning) { + expect(event.severity).toEqual(Severity.Low); + } else { + throw 'wrong event type'; + } + } else { + throw 'wrong row type' + } + }); + + it('parses v2 example analysis', () => { + const report = parse_finished_report(SAMPLE_V2_REPORT_NDJSON); + expect(report.metadata.report_version).toEqual(2); + expect(report.metadata.analyzers).toEqual([ + { + name: 'Analyzer 1', + description: "A first analyzer", + version: 2, + }, + { + name: 'Analyzer 2', + description: "A second analyzer", + version: 2, + }, + ]); + expect(report.rows).toHaveLength(2); + expect(report.rows[0].type).toBe(AnalysisRowType.Skipped); + if (report.rows[1].type === AnalysisRowType.Analysis) { + const row = report.rows[1]; + expect(row.events).toHaveLength(2); + expect(row.events[0]).toBeNull(); + const event = row.events[1]; + const expected_timestamp = new Date('2024-08-19T03:33:54.318Z'); + expect(row.packet_timestamp.getTime()).toEqual(expected_timestamp.getTime()) + if (event !== null && event.type === EventType.Warning) { + expect(event.severity).toEqual(Severity.Low); + } else { + throw 'wrong event type'; + } + } else { + throw 'wrong row type' + } + }); +}); diff --git a/daemon/web/src/lib/analysis.svelte.ts b/daemon/web/src/lib/analysis.svelte.ts index 8a5c8b8..ec25dfb 100644 --- a/daemon/web/src/lib/analysis.svelte.ts +++ b/daemon/web/src/lib/analysis.svelte.ts @@ -13,9 +13,23 @@ export type ReportStatistics = { num_skipped_packets: number; }; -export type ReportMetadata = { - analyzers: AnalyzerMetadata[]; - rayhunter: RayhunterMetadata; +export class ReportMetadata { + public analyzers: AnalyzerMetadata[]; + public rayhunter: RayhunterMetadata; + public report_version: number; + + constructor(ndjson: any) { + this.analyzers = ndjson.analyzers; + this.rayhunter = ndjson.rayhunter; + if (ndjson.report_version === undefined) { + this.report_version = 1; + this.analyzers.forEach(analyzer => { + analyzer.version = 1; + }); + } else { + this.report_version = ndjson.report_version; + } + } }; export type RayhunterMetadata = { @@ -27,19 +41,27 @@ export type RayhunterMetadata = { export type AnalyzerMetadata = { name: string; description: string; + version: number; }; -export type AnalysisRow = { - timestamp: Date; - skipped_message_reasons: string[]; - analysis: PacketAnalysis[]; -}; +export type AnalysisRow = SkippedPacket | PacketAnalysis; +export enum AnalysisRowType { + Skipped, + Analysis, +} + +export type SkippedPacket = { + type: AnalysisRowType.Skipped; + reason: string; +} export type PacketAnalysis = { - timestamp: Date; + type: AnalysisRowType.Analysis; + packet_timestamp: Date; events: Event[]; }; -export type Event = QualitativeWarning | InformationalEvent; + +export type Event = QualitativeWarning | InformationalEvent | null; export enum EventType { Informational, Warning, @@ -62,56 +84,118 @@ export type InformationalEvent = { message: string; }; -export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport { - const metadata: ReportMetadata = report_json[0]; // this can be cast directly - let num_warnings = 0; - let num_informational_logs = 0; - let num_skipped_packets = 0; - const rows: AnalysisRow[] = report_json.slice(1).map((row_json: any) => { - const analysis: PacketAnalysis[] = row_json.analysis.map((analysis_json: any) => { +function get_event(event_json: any): Event { + if (event_json.event_type.type === 'Informational') { + return { + type: EventType.Informational, + message: event_json.message, + }; + } else { + return { + type: EventType.Warning, + severity: + event_json.event_type.severity === 'High' + ? Severity.High + : event_json.event_type.severity === 'Medium' + ? Severity.Medium + : Severity.Low, + message: event_json.message, + }; + } +} + +function get_v1_rows(row_jsons: any[]): AnalysisRow[] { + const rows: AnalysisRow[] = []; + for (const row_json of row_jsons) { + for (const reason of row_json.skipped_message_reasons) { + rows.push({ + type: AnalysisRowType.Skipped, + reason, + }) + } + for (const analysis_json of row_json.analysis) { const events: Event[] = analysis_json.events .map((event_json: any): Event | null => { if (event_json === null) { return null; - } else if (event_json.event_type.type === 'Informational') { - num_informational_logs += 1; - return { - type: EventType.Informational, - message: event_json.message, - }; } else { - num_warnings += 1; - return { - type: EventType.Warning, - severity: - event_json.event_type.severity === 'High' - ? Severity.High - : event_json.event_type.severity === 'Medium' - ? Severity.Medium - : Severity.Low, - message: event_json.message, - }; + return get_event(event_json); } - }) - .filter((maybe_event: Event | null) => maybe_event !== null); - return { - timestamp: analysis_json.timestamp, + }); + rows.push({ + type: AnalysisRowType.Analysis, + packet_timestamp: new Date(analysis_json.timestamp), events, - }; - }); - num_skipped_packets += row_json.skipped_message_reasons.length; - return { - timestamp: new Date(row_json.timestamp), - skipped_message_reasons: row_json.skipped_message_reasons, - analysis, - }; - }); + }); + } + } + return rows; +} + +function get_v2_rows(row_jsons: any[]): AnalysisRow[] { + const rows: AnalysisRow[] = []; + for (const row_json of row_jsons) { + if (row_json.skipped_message_reason) { + rows.push({ + type: AnalysisRowType.Skipped, + reason: row_json.skipped_message_reason, + }); + } else { + const events: Event[] = row_json.events + .map((event_json: any): Event | null => { + if (event_json === null) { + return null; + } else { + return get_event(event_json); + } + }); + rows.push({ + type: AnalysisRowType.Analysis, + packet_timestamp: new Date(row_json.packet_timestamp), + events, + }); + } + } + return rows; +} + +function get_report_stats(rows: AnalysisRow[]): ReportStatistics { + let num_warnings = 0; + let num_informational_logs = 0; + let num_skipped_packets = 0; + for (const row of rows) { + if (row.type === AnalysisRowType.Skipped) { + num_skipped_packets++; + } else { + for (const event of row.events) { + if (event !== null) { + if (event.type === EventType.Informational) { + num_informational_logs++; + } else { + num_warnings++; + } + } + } + } + } return { - statistics: { - num_informational_logs, - num_warnings, - num_skipped_packets, - }, + num_warnings, + num_informational_logs, + num_skipped_packets, + } +} + +export function parse_finished_report(report_json: NewlineDeliminatedJson): AnalysisReport { + const metadata = new ReportMetadata(report_json[0]); + let rows; + if (metadata.report_version === 1) { + rows = get_v1_rows(report_json.slice(1)); + } else { + rows = get_v2_rows(report_json.slice(1)); + } + let statistics = get_report_stats(rows); + return { + statistics, metadata, rows, }; diff --git a/daemon/web/src/lib/components/AnalysisStatus.svelte b/daemon/web/src/lib/components/AnalysisStatus.svelte index 6a9579f..54df7e1 100644 --- a/daemon/web/src/lib/components/AnalysisStatus.svelte +++ b/daemon/web/src/lib/components/AnalysisStatus.svelte @@ -1,6 +1,6 @@