Skip to content

Commit c2cb8cc

Browse files
committed
check: add --json formatter
1 parent cf1b4e2 commit c2cb8cc

File tree

1 file changed

+133
-16
lines changed

1 file changed

+133
-16
lines changed

check/src/main.rs

Lines changed: 133 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,41 @@
1-
use clap::Parser;
1+
use chrono::{DateTime, FixedOffset};
2+
use clap::{Parser, ValueEnum};
23
use futures::TryStreamExt;
34
use log::{debug, error, info, warn};
45
use pcap_file_tokio::pcapng::{Block, PcapNgReader};
56
use rayhunter::{
6-
analysis::analyzer::{AnalysisRow, AnalyzerConfig, EventType, Harness},
7+
analysis::analyzer::{AnalysisRow, AnalyzerConfig, Event, EventType, Harness},
78
diag::DataType,
89
gsmtap_parser,
910
pcap::GsmtapPcapWriter,
1011
qmdl::QmdlReader,
1112
};
13+
use serde_json;
1214
use std::{collections::HashMap, future, path::PathBuf, pin::pin};
1315
use tokio::fs::File;
1416
use walkdir::WalkDir;
1517

18+
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
19+
enum OutputFormat {
20+
Text,
21+
Json,
22+
}
23+
24+
impl Default for OutputFormat {
25+
fn default() -> Self {
26+
OutputFormat::Text
27+
}
28+
}
29+
30+
impl std::fmt::Display for OutputFormat {
31+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32+
match self {
33+
OutputFormat::Text => write!(f, "text"),
34+
OutputFormat::Json => write!(f, "json"),
35+
}
36+
}
37+
}
38+
1639
#[derive(Parser, Debug)]
1740
#[command(version, about)]
1841
struct Args {
@@ -22,6 +45,9 @@ struct Args {
2245
#[arg(short = 'P', long, help = "Convert qmdl files to pcap before analysis")]
2346
pcapify: bool,
2447

48+
#[arg(short, long, default_value_t = OutputFormat::Text)]
49+
format: OutputFormat,
50+
2551
#[arg(long, help = "Show why some packets were skipped during analysis")]
2652
show_skipped: bool,
2753

@@ -32,23 +58,30 @@ struct Args {
3258
debug: bool,
3359
}
3460

61+
trait Reporter {
62+
fn process_row(&mut self, row: AnalysisRow);
63+
fn finish(&self, show_skipped: bool) -> std::io::Result<()>;
64+
}
65+
3566
#[derive(Default)]
36-
struct Report {
67+
struct TextReporter {
3768
skipped_reasons: HashMap<String, u32>,
3869
total_messages: u32,
3970
warnings: u32,
4071
skipped: u32,
4172
file_path: String,
4273
}
4374

44-
impl Report {
75+
impl TextReporter {
4576
fn new(file_path: &str) -> Self {
46-
Report {
77+
TextReporter {
4778
file_path: file_path.to_string(),
4879
..Default::default()
4980
}
5081
}
82+
}
5183

84+
impl Reporter for TextReporter {
5285
fn process_row(&mut self, row: AnalysisRow) {
5386
self.total_messages += 1;
5487
if let Some(reason) = row.skipped_message_reason {
@@ -76,7 +109,7 @@ impl Report {
76109
}
77110
}
78111

79-
fn print_summary(&self, show_skipped: bool) {
112+
fn finish(&self, show_skipped: bool) -> std::result::Result<(), std::io::Error> {
80113
if show_skipped && self.skipped > 0 {
81114
info!("{}: messages skipped:", self.file_path);
82115
for (reason, count) in self.skipped_reasons.iter() {
@@ -87,16 +120,90 @@ impl Report {
87120
"{}: {} messages analyzed, {} warnings, {} messages skipped",
88121
self.file_path, self.total_messages, self.warnings, self.skipped
89122
);
123+
Ok(())
90124
}
91125
}
92126

93-
async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
127+
#[derive(serde::Serialize)]
128+
struct JsonEvent {
129+
timestamp: DateTime<FixedOffset>,
130+
event: Event,
131+
}
132+
133+
#[derive(Default)]
134+
struct JsonReporter {
135+
skipped_reasons: HashMap<String, u32>,
136+
total_messages: u32,
137+
warnings: u32,
138+
skipped: u32,
139+
file_path: String,
140+
events: Vec<JsonEvent>,
141+
}
142+
143+
impl JsonReporter {
144+
fn new(file_path: &str) -> Self {
145+
JsonReporter {
146+
file_path: file_path.to_string(),
147+
..Default::default()
148+
}
149+
}
150+
}
151+
152+
impl Reporter for JsonReporter {
153+
fn process_row(&mut self, row: AnalysisRow) {
154+
self.total_messages += 1;
155+
if let Some(reason) = row.skipped_message_reason {
156+
*self.skipped_reasons.entry(reason).or_insert(0) += 1;
157+
self.skipped += 1;
158+
return;
159+
}
160+
for maybe_event in row.events {
161+
let Some(event) = maybe_event else { continue };
162+
let Some(timestamp) = row.packet_timestamp else {
163+
continue;
164+
};
165+
166+
match &event.event_type {
167+
EventType::Low | EventType::Medium | EventType::High => {
168+
self.warnings += 1;
169+
}
170+
_ => (),
171+
}
172+
173+
self.events.push(JsonEvent { timestamp, event });
174+
}
175+
}
176+
177+
fn finish(&self, show_skipped: bool) -> std::result::Result<(), std::io::Error> {
178+
let mut report: serde_json::Value = serde_json::json!({
179+
"file_path": &self.file_path,
180+
"events": &self.events,
181+
"summary": {
182+
"total_messages": self.total_messages,
183+
"warnings": self.warnings,
184+
}
185+
});
186+
187+
if show_skipped && self.skipped > 0 {
188+
report.as_object_mut().unwrap().insert(
189+
"skipped_messages".to_string(),
190+
serde_json::json!({
191+
"total_skipped": self.skipped,
192+
"reasons": &self.skipped_reasons,
193+
}),
194+
);
195+
}
196+
197+
serde_json::to_writer_pretty(std::io::stdout(), &report).map_err(|e| From::from(e))
198+
}
199+
}
200+
201+
async fn analyze_pcap(pcap_path: &str, show_skipped: bool, mut reporter: Box<dyn Reporter>) {
94202
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
95203
let pcap_file = &mut File::open(&pcap_path).await.expect("failed to open file");
96204
let mut pcap_reader = PcapNgReader::new(pcap_file)
97205
.await
98206
.expect("failed to read PCAP file");
99-
let mut report = Report::new(pcap_path);
100207
while let Some(Ok(block)) = pcap_reader.next_block().await {
101208
let row = match block {
102209
Block::EnhancedPacket(packet) => harness.analyze_pcap_packet(packet),
@@ -105,12 +212,15 @@ async fn analyze_pcap(pcap_path: &str, show_skipped: bool) {
105212
continue;
106213
}
107214
};
108-
report.process_row(row);
215+
reporter.process_row(row);
216+
}
217+
218+
if let Err(e) = reporter.finish(show_skipped) {
219+
error!("Couldn't generate report summary: {e}");
109220
}
110-
report.print_summary(show_skipped);
111221
}
112222

113-
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
223+
async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool, mut reporter: Box<dyn Reporter>) {
114224
let mut harness = Harness::new_with_config(&AnalyzerConfig::default());
115225
let qmdl_file = &mut File::open(&qmdl_path).await.expect("failed to open file");
116226
let file_size = qmdl_file
@@ -124,17 +234,19 @@ async fn analyze_qmdl(qmdl_path: &str, show_skipped: bool) {
124234
.as_stream()
125235
.try_filter(|container| future::ready(container.data_type == DataType::UserSpace))
126236
);
127-
let mut report = Report::new(qmdl_path);
128237
while let Some(container) = qmdl_stream
129238
.try_next()
130239
.await
131240
.expect("failed getting QMDL container")
132241
{
133242
for row in harness.analyze_qmdl_messages(container) {
134-
report.process_row(row);
243+
reporter.process_row(row);
135244
}
136245
}
137-
report.print_summary(show_skipped);
246+
247+
if let Err(e) = reporter.finish(show_skipped) {
248+
error!("Couldn't generate report summary: {e}");
249+
}
138250
}
139251

140252
async fn pcapify(qmdl_path: &PathBuf) {
@@ -206,16 +318,21 @@ async fn main() {
206318
let path_str = path.to_str().unwrap();
207319
// instead of relying on the QMDL extension, can we check if a file is
208320
// QMDL by inspecting the contents?
321+
322+
let mut reporter: Box<dyn Reporter> = match args.format {
323+
OutputFormat::Text => Box::new(TextReporter::new(path_str)),
324+
OutputFormat::Json => Box::new(JsonReporter::new(path_str)),
325+
};
209326
if name_str.ends_with(".qmdl") {
210327
info!("**** Beginning analysis of {name_str}");
211-
analyze_qmdl(path_str, args.show_skipped).await;
328+
analyze_qmdl(path_str, args.show_skipped, reporter).await;
212329
if args.pcapify {
213330
pcapify(&path.to_path_buf()).await;
214331
}
215332
} else if name_str.ends_with(".pcap") || name_str.ends_with(".pcapng") {
216333
// TODO: if we've already analyzed a QMDL, skip its corresponding pcap
217334
info!("**** Beginning analysis of {name_str}");
218-
analyze_pcap(path_str, args.show_skipped).await;
335+
analyze_pcap(path_str, args.show_skipped, reporter).await;
219336
}
220337
}
221338
}

0 commit comments

Comments
 (0)