1- use clap:: Parser ;
1+ use chrono:: { DateTime , FixedOffset } ;
2+ use clap:: { Parser , ValueEnum } ;
23use futures:: TryStreamExt ;
34use log:: { debug, error, info, warn} ;
45use pcap_file_tokio:: pcapng:: { Block , PcapNgReader } ;
56use 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;
1214use std:: { collections:: HashMap , future, path:: PathBuf , pin:: pin} ;
1315use tokio:: fs:: File ;
1416use 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) ]
1841struct 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
140252async 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