Skip to main content

fukurow_cli/
commands.rs

1//! CLI command definitions and handlers
2
3use 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/// Main CLI structure
11#[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/// Available CLI commands
21#[derive(Subcommand)]
22pub enum Commands {
23    /// Start the API server
24    Serve {
25        /// Host to bind to
26        #[arg(long, default_value = "0.0.0.0")]
27        host: String,
28
29        /// Port to bind to
30        #[arg(short, long, default_value = "3000")]
31        port: u16,
32    },
33
34    /// Analyze a single event
35    Analyze {
36        /// Event data as JSON file
37        #[arg(short, long)]
38        file: Option<PathBuf>,
39
40        /// Event data as JSON string
41        #[arg(short, long)]
42        json: Option<String>,
43
44        /// Output format
45        #[arg(short, long, default_value = "text")]
46        format: OutputFormat,
47    },
48
49    /// Process events from file
50    Process {
51        /// Input file containing events
52        #[arg(short, long)]
53        input: PathBuf,
54
55        /// Output file for results
56        #[arg(short, long)]
57        output: Option<PathBuf>,
58
59        /// Output format
60        #[arg(short, long, default_value = "json")]
61        format: OutputFormat,
62    },
63
64    /// Query the knowledge graph
65    Query {
66        /// Subject filter
67        #[arg(short, long)]
68        subject: Option<String>,
69
70        /// Predicate filter
71        #[arg(short, long)]
72        predicate: Option<String>,
73
74        /// Object filter
75        #[arg(short, long)]
76        object: Option<String>,
77
78        /// Output format
79        #[arg(short, long, default_value = "text")]
80        format: OutputFormat,
81    },
82
83    /// Threat intelligence operations
84    Threat {
85        #[command(subcommand)]
86        command: ThreatCommands,
87    },
88
89    /// Show system information
90    Info,
91}
92
93/// Threat intelligence subcommands
94#[derive(Subcommand)]
95pub enum ThreatCommands {
96    /// Show threat intelligence statistics
97    Stats,
98
99    /// Export threat indicators
100    Export {
101        /// Output file
102        #[arg(short, long)]
103        output: Option<PathBuf>,
104    },
105
106    /// Import threat indicators
107    Import {
108        /// Input file containing indicators
109        #[arg(short, long)]
110        input: PathBuf,
111    },
112
113    /// Check if value is a known threat
114    Check {
115        /// Value to check
116        #[arg(short, long)]
117        value: String,
118
119        /// Type of indicator (ip, domain, hash, etc.)
120        #[arg(short, long)]
121        r#type: String,
122    },
123}
124
125/// Output format options
126#[derive(Clone, Debug, PartialEq, clap::ValueEnum)]
127pub enum OutputFormat {
128    Text,
129    Json,
130    JsonPretty,
131}
132
133/// Command execution result
134pub struct CommandResult {
135    pub success: bool,
136    pub message: String,
137    pub data: Option<serde_json::Value>,
138}
139
140/// Execute CLI commands
141pub struct CommandExecutor {
142    reasoner: ReasonerEngine,
143    threat_processor: ThreatProcessor,
144}
145
146impl CommandExecutor {
147    pub fn new() -> Self {
148        let reasoner = ReasonerEngine::new();
149        // Note: Adding default rules to reasoner would require API change
150
151        Self {
152            reasoner,
153            threat_processor: ThreatProcessor::new(),
154        }
155    }
156
157    /// Execute a CLI command
158    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                // Note: Would need mutable access to threat processor
339                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                // Parse indicator type
349                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}