1use clap::{Parser, Subcommand};
4use fukurow_engine::ReasonerEngine;
5use fukurow_core::model::CyberEvent;
6use fukurow_domain_cyber::threat_intelligence::{ThreatProcessor, IndicatorType};
7use std::path::PathBuf;
8use anyhow::Result;
9
10#[derive(Parser)]
12#[command(name = "reasoner")]
13#[command(about = "JSON-LD Reasoner for Cyber Security Events")]
14#[command(version = env!("CARGO_PKG_VERSION"))]
15pub struct Cli {
16 #[command(subcommand)]
17 pub command: Commands,
18}
19
20#[derive(Subcommand)]
22pub enum Commands {
23 Serve {
25 #[arg(long, default_value = "0.0.0.0")]
27 host: String,
28
29 #[arg(short, long, default_value = "3000")]
31 port: u16,
32 },
33
34 Analyze {
36 #[arg(short, long)]
38 file: Option<PathBuf>,
39
40 #[arg(short, long)]
42 json: Option<String>,
43
44 #[arg(short, long, default_value = "text")]
46 format: OutputFormat,
47 },
48
49 Process {
51 #[arg(short, long)]
53 input: PathBuf,
54
55 #[arg(short, long)]
57 output: Option<PathBuf>,
58
59 #[arg(short, long, default_value = "json")]
61 format: OutputFormat,
62 },
63
64 Query {
66 #[arg(short, long)]
68 subject: Option<String>,
69
70 #[arg(short, long)]
72 predicate: Option<String>,
73
74 #[arg(short, long)]
76 object: Option<String>,
77
78 #[arg(short, long, default_value = "text")]
80 format: OutputFormat,
81 },
82
83 Threat {
85 #[command(subcommand)]
86 command: ThreatCommands,
87 },
88
89 Info,
91}
92
93#[derive(Subcommand)]
95pub enum ThreatCommands {
96 Stats,
98
99 Export {
101 #[arg(short, long)]
103 output: Option<PathBuf>,
104 },
105
106 Import {
108 #[arg(short, long)]
110 input: PathBuf,
111 },
112
113 Check {
115 #[arg(short, long)]
117 value: String,
118
119 #[arg(short, long)]
121 r#type: String,
122 },
123}
124
125#[derive(Clone, Debug, PartialEq, clap::ValueEnum)]
127pub enum OutputFormat {
128 Text,
129 Json,
130 JsonPretty,
131}
132
133pub struct CommandResult {
135 pub success: bool,
136 pub message: String,
137 pub data: Option<serde_json::Value>,
138}
139
140pub struct CommandExecutor {
142 reasoner: ReasonerEngine,
143 threat_processor: ThreatProcessor,
144}
145
146impl CommandExecutor {
147 pub fn new() -> Self {
148 let reasoner = ReasonerEngine::new();
149 Self {
152 reasoner,
153 threat_processor: ThreatProcessor::new(),
154 }
155 }
156
157 pub async fn execute(&mut self, command: Commands) -> Result<CommandResult> {
159 match command {
160 Commands::Serve { host, port } => self.execute_serve(host, port).await,
161 Commands::Analyze { file, json, format } => self.execute_analyze(file, json, format).await,
162 Commands::Process { input, output, format } => self.execute_process(input, output, format).await,
163 Commands::Query { subject, predicate, object, format } => {
164 self.execute_query(subject, predicate, object, format).await
165 }
166 Commands::Threat { command } => self.execute_threat_command(command).await,
167 Commands::Info => self.execute_info(),
168 }
169 }
170
171 async fn execute_serve(&self, host: String, port: u16) -> Result<CommandResult> {
172 use fukurow_api::{ReasonerServer, ServerConfig};
173
174 let config = ServerConfig { host: host.clone(), port, max_connections: 100 };
175 let server = ReasonerServer::with_config(config);
176
177 println!("Starting server on {}:{}", host, port);
178 println!("Press Ctrl+C to stop");
179
180 server.serve().await?;
181
182 Ok(CommandResult {
183 success: true,
184 message: "Server stopped".to_string(),
185 data: None,
186 })
187 }
188
189 async fn execute_analyze(
190 &mut self,
191 file: Option<PathBuf>,
192 json: Option<String>,
193 format: OutputFormat,
194 ) -> Result<CommandResult> {
195 let event_data = if let Some(file_path) = file {
196 std::fs::read_to_string(file_path)?
197 } else if let Some(json_str) = json {
198 json_str
199 } else {
200 return Err(anyhow::anyhow!("Either --file or --json must be specified"));
201 };
202
203 let event: CyberEvent = serde_json::from_str(&event_data)?;
204
205 self.reasoner.add_event(event).await?;
206 let actions = self.reasoner.reason().await?;
207
208 match format {
209 OutputFormat::Text => {
210 println!("Event analyzed successfully\nActions proposed: {}", actions.len());
211 for (i, action) in actions.iter().enumerate() {
212 println!("{}. {:?}", i + 1, action);
213 }
214 }
215 OutputFormat::Json => println!("{}", serde_json::to_string(&actions)?),
216 OutputFormat::JsonPretty => println!("{}", serde_json::to_string_pretty(&actions)?),
217 }
218
219 Ok(CommandResult {
220 success: true,
221 message: "Event analyzed".to_string(),
222 data: Some(serde_json::json!({ "actions": actions })),
223 })
224 }
225
226 async fn execute_process(
227 &mut self,
228 input: PathBuf,
229 output: Option<PathBuf>,
230 format: OutputFormat,
231 ) -> Result<CommandResult> {
232 let content = std::fs::read_to_string(&input)?;
233 let events: Vec<CyberEvent> = serde_json::from_str(&content)?;
234
235 let mut all_actions = Vec::new();
236 let mut processed_count = 0;
237
238 for event in events {
239 self.reasoner.add_event(event).await?;
240 processed_count += 1;
241 }
242
243 let actions = self.reasoner.reason().await?;
244 all_actions.extend(actions);
245
246 let result = match format {
247 OutputFormat::Text => {
248 format!("Processed {} events, generated {} actions", processed_count, all_actions.len())
249 }
250 OutputFormat::Json => serde_json::to_string(&all_actions)?,
251 OutputFormat::JsonPretty => serde_json::to_string_pretty(&all_actions)?,
252 };
253
254 if let Some(output_path) = output {
255 std::fs::write(output_path, &result)?;
256 } else {
257 println!("{}", result);
258 }
259
260 Ok(CommandResult {
261 success: true,
262 message: format!("Processed {} events", processed_count),
263 data: Some(serde_json::json!({
264 "processed_events": processed_count,
265 "actions": all_actions
266 })),
267 })
268 }
269
270 async fn execute_query(
271 &self,
272 subject: Option<String>,
273 predicate: Option<String>,
274 object: Option<String>,
275 format: OutputFormat,
276 ) -> Result<CommandResult> {
277 let store = self.reasoner.get_graph_store().await;
278 let graph_store = store.read().await;
279
280 let triples = graph_store.find_triples(
281 subject.as_deref(),
282 predicate.as_deref(),
283 object.as_deref(),
284 );
285
286 let count = triples.len();
287 let result = match format {
288 OutputFormat::Text => {
289 let mut output = format!("Found {} triples:\n", count);
290 for triple in &triples {
291 output.push_str(&format!(" {} {} {}\n", triple.triple.subject, triple.triple.predicate, triple.triple.object));
292 }
293 output
294 }
295 OutputFormat::Json => serde_json::to_string(&triples)?,
296 OutputFormat::JsonPretty => serde_json::to_string_pretty(&triples)?,
297 };
298
299 println!("{}", result);
300
301 Ok(CommandResult {
302 success: true,
303 message: format!("Found {} triples", count),
304 data: Some(serde_json::json!({ "triples": triples })),
305 })
306 }
307
308 async fn execute_threat_command(&self, command: ThreatCommands) -> Result<CommandResult> {
309 match command {
310 ThreatCommands::Stats => {
311 let stats = self.threat_processor.get_statistics();
312 let result = serde_json::to_string_pretty(&stats)?;
313 println!("{}", result);
314
315 Ok(CommandResult {
316 success: true,
317 message: "Threat intelligence statistics".to_string(),
318 data: Some(serde_json::json!(stats)),
319 })
320 }
321 ThreatCommands::Export { output } => {
322 let json_data = self.threat_processor.export_indicators()?;
323
324 if let Some(output_path) = output {
325 std::fs::write(output_path, &json_data)?;
326 } else {
327 println!("{}", json_data);
328 }
329
330 Ok(CommandResult {
331 success: true,
332 message: "Threat indicators exported".to_string(),
333 data: Some(serde_json::json!({ "exported": true })),
334 })
335 }
336 ThreatCommands::Import { input } => {
337 let _content = std::fs::read_to_string(input)?;
338 println!("Import functionality not yet implemented");
340
341 Ok(CommandResult {
342 success: false,
343 message: "Import not implemented".to_string(),
344 data: None,
345 })
346 }
347 ThreatCommands::Check { value, r#type } => {
348 let indicator_type = match r#type.as_str() {
350 "ip" => IndicatorType::IpAddress,
351 "domain" => IndicatorType::Domain,
352 "hash" => IndicatorType::FileHash,
353 _ => return Err(anyhow::anyhow!("Unknown indicator type: {}", r#type)),
354 };
355
356 let is_threat = self.threat_processor.process_event(&value, indicator_type);
357
358 let result = if let Some(ref threat_info) = is_threat {
359 format!("THREAT DETECTED: {} - {}", value, threat_info)
360 } else {
361 format!("No threat detected for: {}", value)
362 };
363
364 println!("{}", result);
365
366 Ok(CommandResult {
367 success: true,
368 message: "Threat check completed".to_string(),
369 data: Some(serde_json::json!({
370 "value": value,
371 "type": r#type,
372 "is_threat": is_threat.is_some(),
373 "threat_info": is_threat
374 })),
375 })
376 }
377 }
378 }
379
380 fn execute_info(&self) -> Result<CommandResult> {
381 let info = serde_json::json!({
382 "name": env!("CARGO_PKG_NAME"),
383 "version": env!("CARGO_PKG_VERSION"),
384 "description": env!("CARGO_PKG_DESCRIPTION"),
385 "authors": env!("CARGO_PKG_AUTHORS"),
386 "repository": env!("CARGO_PKG_REPOSITORY"),
387 "rust_version": "1.70+",
388 "features": ["jsonld", "cyber-security", "rest-api", "cli"]
389 });
390
391 let result = serde_json::to_string_pretty(&info)?;
392 println!("{}", result);
393
394 Ok(CommandResult {
395 success: true,
396 message: "System information".to_string(),
397 data: Some(info),
398 })
399 }
400}
401
402impl Default for CommandExecutor {
403 fn default() -> Self {
404 Self::new()
405 }
406}