nabla_cli/cli/
mod.rs

1use anyhow::Result;
2use clap::Subcommand;
3use reqwest::{Client, multipart};
4use serde_json::json;
5use sha2::{Digest, Sha256};
6use std::path::{Path, PathBuf};
7
8mod auth;
9mod config;
10mod jwt_store;
11
12use crate::ssrf_protection::SSRFValidator;
13pub use auth::AuthArgs;
14pub use config::{ConfigCommands, ConfigStore};
15pub use jwt_store::*;
16
17const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024; // 100MB limit
18
19fn validate_file_path(file_path: &str) -> Result<PathBuf> {
20    // Remove any @ prefix if present
21    let clean_path = if file_path.starts_with('@') {
22        &file_path[1..]
23    } else {
24        file_path
25    };
26
27    let path = Path::new(clean_path);
28
29    // Check if file exists
30    if !path.exists() {
31        return Err(anyhow::anyhow!("File not found: {}", clean_path));
32    }
33
34    // Get canonical path to resolve any .. or . components
35    let canonical_path = path
36        .canonicalize()
37        .map_err(|e| anyhow::anyhow!("Invalid file path '{}': {}", clean_path, e))?;
38
39    // Get current working directory
40    let current_dir = std::env::current_dir()
41        .map_err(|e| anyhow::anyhow!("Cannot determine current directory: {}", e))?;
42
43    // Ensure the canonical path is within or below the current working directory
44    if !canonical_path.starts_with(&current_dir) {
45        return Err(anyhow::anyhow!(
46            "Access denied: file '{}' is outside the current working directory",
47            clean_path
48        ));
49    }
50
51    // Check file size
52    let metadata = std::fs::metadata(&canonical_path)
53        .map_err(|e| anyhow::anyhow!("Cannot read file metadata: {}", e))?;
54
55    if metadata.len() > MAX_FILE_SIZE {
56        return Err(anyhow::anyhow!(
57            "File too large: {} bytes (max: {} bytes)",
58            metadata.len(),
59            MAX_FILE_SIZE
60        ));
61    }
62
63    // Ensure it's a regular file, not a symlink or directory
64    if !metadata.is_file() {
65        return Err(anyhow::anyhow!(
66            "Path must be a regular file: {}",
67            clean_path
68        ));
69    }
70
71    Ok(canonical_path)
72}
73
74#[derive(Subcommand)]
75pub enum Commands {
76    Auth {
77        #[command(flatten)]
78        args: AuthArgs,
79    },
80    Config {
81        #[command(subcommand)]
82        command: ConfigCommands,
83    },
84    Binary {
85        #[command(subcommand)]
86        command: BinaryCommands,
87    },
88    Diff {
89        file1: String,
90        file2: String,
91    },
92    Chat {
93        message: String,
94    },
95    Upgrade,
96    Server {
97        #[arg(long, default_value = "8080")]
98        port: u16,
99    },
100}
101
102#[derive(Subcommand)]
103pub enum BinaryCommands {
104    Analyze {
105        file: String,
106    },
107    Attest {
108        file: String,
109        #[arg(long)]
110        signing_key: String,
111    },
112    CheckCves {
113        file: String,
114    },
115}
116
117pub struct NablaCli {
118    jwt_store: JwtStore,
119    config_store: ConfigStore,
120    http_client: Client,
121}
122
123impl NablaCli {
124    pub fn new() -> Result<Self> {
125        Ok(Self {
126            jwt_store: JwtStore::new()?,
127            config_store: ConfigStore::new()?,
128            http_client: Client::new(),
129        })
130    }
131
132    pub async fn show_intro_and_help(&self) -> Result<()> {
133        self.print_ascii_intro();
134        self.print_help();
135        Ok(())
136    }
137
138    pub async fn handle_command(&mut self, command: Commands) -> Result<()> {
139        match command {
140            Commands::Auth { args } => self.handle_auth_args(args),
141            Commands::Config { command } => self.handle_config_command(command),
142            Commands::Binary { command } => self.handle_binary_command(command).await,
143            Commands::Diff { file1, file2 } => self.handle_diff_command(&file1, &file2).await,
144            Commands::Chat { message } => self.handle_chat_command(&message).await,
145            Commands::Upgrade => self.handle_upgrade_command(),
146            Commands::Server { port } => self.handle_server_command(port).await,
147        }
148    }
149
150    fn handle_config_command(&mut self, command: ConfigCommands) -> Result<()> {
151        match command {
152            ConfigCommands::Get { key } => {
153                let value = self.config_store.get_setting(&key)?;
154                match value {
155                    Some(val) => println!("{}: {}", key, val),
156                    None => println!("No value set for key: {}", key),
157                }
158                Ok(())
159            }
160            ConfigCommands::Set { key, value } => {
161                self.config_store.set_setting(&key, &value)?;
162                println!("Set {} = {}", key, value);
163                Ok(())
164            }
165            ConfigCommands::SetBaseUrl { url } => self.config_store.set_base_url(&url),
166            ConfigCommands::List => {
167                let settings = self.config_store.list_settings()?;
168                if settings.is_empty() {
169                    println!("No configuration settings found.");
170                } else {
171                    println!("Configuration settings:");
172                    for (key, value) in settings {
173                        println!("  {}: {}", key, value);
174                    }
175                }
176                Ok(())
177            }
178        }
179    }
180
181    fn print_ascii_intro(&self) {
182        println!(
183            r#"
184    ███╗   ██╗ █████╗ ██████╗ ██╗      █████╗ 
185    ████╗  ██║██╔══██╗██╔══██╗██║     ██╔══██╗
186    ██╔██╗ ██║███████║██████╔╝██║     ███████║
187    ██║╚██╗██║██╔══██║██╔══██╗██║     ██╔══██║
188    ██║ ╚████║██║  ██║██████╔╝███████╗██║  ██║
189    ╚═╝  ╚═══╝╚═╝  ╚═╝╚═════╝ ╚══════╝╚═╝  ╚═╝
190                                              
191    🔒 Binary Analysis & Security Platform
192        "#
193        );
194    }
195
196    fn print_help(&self) {
197        println!("Available Commands:");
198        println!();
199        println!("🔐 Authentication:");
200        println!("  nabla auth upgrade      - Upgrade your plan");
201        println!("  nabla auth status       - Check authentication status");
202        println!("  nabla auth --set-jwt <token> - Set JWT token for authentication");
203        println!();
204        println!("⚙️  Configuration:");
205        println!("  nabla config get <key>      - Get configuration value");
206        println!("  nabla config set <key> <val> - Set configuration value");
207        println!("  nabla config set-base-url <url> - Set base URL for API requests");
208        println!("  nabla config list           - List all configuration");
209        println!();
210        println!("🔍 Binary Analysis:");
211        println!("  nabla binary analyze <file>  - Analyze a binary file");
212        println!("  nabla binary attest --signing-key <key> <file> - Create signed attestation");
213        println!("  nabla binary check-cves <file> - Check for CVEs");
214        println!();
215        println!("🔍 Comparison:");
216        println!("  nabla diff <file1> <file2>   - Compare two binaries");
217        println!();
218        println!("💬 Chat (Premium Feature):");
219        println!("  nabla chat <message>         - Chat about analysis");
220        println!();
221        println!("🚀 Upgrade:");
222        println!("  nabla upgrade               - Upgrade to AWS Marketplace plan");
223        println!();
224        println!("🖥️  Server:");
225        println!("  nabla server --port <port>  - Start HTTP server (default: 8080)");
226        println!();
227        println!("💡 Tip: Run 'nabla upgrade' to unlock premium features!");
228    }
229
230    async fn handle_binary_command(&mut self, command: BinaryCommands) -> Result<()> {
231        match command {
232            BinaryCommands::Analyze { file } => self.handle_analyze_command(&file).await,
233            BinaryCommands::Attest { file, signing_key } => {
234                self.handle_attest_command(&file, signing_key).await
235            }
236            BinaryCommands::CheckCves { file } => self.handle_check_cves_command(&file).await,
237        }
238    }
239
240    async fn handle_analyze_command(&mut self, file_path: &str) -> Result<()> {
241        let validated_path = validate_file_path(file_path)?;
242
243        println!("🔍 Analyzing binary: {}", validated_path.display());
244
245        let jwt_data = self.jwt_store.load_jwt().ok().flatten();
246        let base_url = self.config_store.get_base_url()?;
247        let url = format!("{}/binary/analyze", base_url);
248
249        let file_content = std::fs::read(&validated_path)?;
250        let file_name = validated_path
251            .file_name()
252            .unwrap_or_default()
253            .to_string_lossy()
254            .to_string();
255
256        println!("🔄 Uploading to analysis endpoint...");
257
258        let ssrf_validator = SSRFValidator::new();
259        let validated_url = ssrf_validator.validate_url(&url)?;
260
261        let part = multipart::Part::bytes(file_content).file_name(file_name);
262        let form = multipart::Form::new().part("file", part);
263
264        let mut request = self.http_client.post(validated_url.to_string());
265        if let Some(jwt) = jwt_data.as_ref() {
266            request = request.bearer_auth(&jwt.token);
267        }
268
269        let response = request.multipart(form).send().await?;
270        let result = response.json::<serde_json::Value>().await?;
271
272        println!("✅ Analysis complete!");
273        println!("Results: {}", serde_json::to_string_pretty(&result)?);
274
275        Ok(())
276    }
277
278    async fn handle_attest_command(&mut self, file_path: &str, signing_key: String) -> Result<()> {
279        let jwt_data = self.jwt_store.load_jwt()?
280            .ok_or_else(|| anyhow::anyhow!("Authentication required for binary attestation. Run 'nabla auth --set-jwt <token>' or 'nabla upgrade'"))?;
281
282        let validated_file_path = validate_file_path(file_path)?;
283        let validated_key_path = validate_file_path(&signing_key)?;
284
285        println!("🔐 Attesting binary: {}", validated_file_path.display());
286
287        let base_url = self.config_store.get_base_url()?;
288        let url = format!("{}/binary/attest", base_url);
289
290        let file_content = std::fs::read(&validated_file_path)?;
291        let file_name = validated_file_path
292            .file_name()
293            .unwrap_or_default()
294            .to_string_lossy()
295            .to_string();
296        let signing_key_content = std::fs::read(&validated_key_path)?;
297
298        println!("🔄 Uploading to attestation endpoint...");
299
300        let ssrf_validator = SSRFValidator::new();
301        let validated_url = ssrf_validator.validate_url(&url)?;
302
303        let file_part = multipart::Part::bytes(file_content.clone()).file_name(file_name.clone());
304        let key_part = multipart::Part::bytes(signing_key_content).file_name("key.pem");
305        let form = multipart::Form::new()
306            .part("file", file_part)
307            .part("signing_key", key_part);
308
309        let response = self
310            .http_client
311            .post(validated_url.to_string())
312            .bearer_auth(&jwt_data.token)
313            .multipart(form)
314            .send()
315            .await?;
316        let result = response.json::<serde_json::Value>().await?;
317
318        // Mock attestation using SHA-256
319        let mut hasher = Sha256::new();
320        hasher.update(&file_content);
321        let hash = hasher.finalize();
322        let attestation = json!({
323            "_type": "https://in-toto.io/Statement/v0.1",
324            "subject": [{
325                "name": file_name,
326                "digest": {
327                    "sha256": format!("{:x}", hash)
328                }
329            }],
330            "predicateType": "https://nabla.sh/attestation/v0.1",
331            "predicate": {
332                "timestamp": chrono::Utc::now().to_rfc3339(),
333                "analysis": {
334                    "format": "ELF",
335                    "architecture": "x86_64",
336                    "security_score": 85
337                }
338            }
339        });
340
341        println!("✅ Attestation complete!");
342        println!(
343            "Results: {}",
344            serde_json::to_string_pretty(&json!({
345                "analysis": result,
346                "attestation": attestation
347            }))?
348        );
349
350        Ok(())
351    }
352
353    async fn handle_check_cves_command(&mut self, file_path: &str) -> Result<()> {
354        let validated_path = validate_file_path(file_path)?;
355        let jwt_data = self.jwt_store.load_jwt().ok().flatten();
356
357        println!("🔍 Checking CVEs for: {}", validated_path.display());
358
359        let base_url = self.config_store.get_base_url()?;
360        let url = format!("{}/binary/check-cves", base_url);
361
362        let file_content = std::fs::read(&validated_path)?;
363        let file_name = validated_path
364            .file_name()
365            .unwrap_or_default()
366            .to_string_lossy()
367            .to_string();
368
369        println!("🔄 Uploading to CVE check endpoint...");
370
371        let ssrf_validator = SSRFValidator::new();
372        let validated_url = ssrf_validator.validate_url(&url)?;
373
374        let part = multipart::Part::bytes(file_content).file_name(file_name);
375        let form = multipart::Form::new().part("file", part);
376
377        let mut request = self.http_client.post(validated_url.to_string());
378        if let Some(jwt) = jwt_data.as_ref() {
379            request = request.bearer_auth(&jwt.token);
380        }
381
382        let response = request.multipart(form).send().await?;
383        let result = response.json::<serde_json::Value>().await?;
384
385        println!("✅ CVE check complete!");
386        println!("Results: {}", serde_json::to_string_pretty(&result)?);
387
388        Ok(())
389    }
390
391    async fn handle_diff_command(&mut self, file1: &str, file2: &str) -> Result<()> {
392        let validated_path1 = validate_file_path(file1)?;
393        let validated_path2 = validate_file_path(file2)?;
394        let jwt_data = self.jwt_store.load_jwt().ok().flatten();
395
396        println!(
397            "🔍 Comparing binaries: {} vs {}",
398            validated_path1.display(),
399            validated_path2.display()
400        );
401
402        let base_url = self.config_store.get_base_url()?;
403        let url = format!("{}/binary/diff", base_url);
404
405        let file1_content = std::fs::read(&validated_path1)?;
406        let file2_content = std::fs::read(&validated_path2)?;
407        let file1_name = validated_path1
408            .file_name()
409            .unwrap_or_default()
410            .to_string_lossy()
411            .to_string();
412        let file2_name = validated_path2
413            .file_name()
414            .unwrap_or_default()
415            .to_string_lossy()
416            .to_string();
417
418        println!("🔄 Uploading files to diff endpoint...");
419
420        let ssrf_validator = SSRFValidator::new();
421        let validated_url = ssrf_validator.validate_url(&url)?;
422
423        let file1_part = multipart::Part::bytes(file1_content).file_name(file1_name);
424        let file2_part = multipart::Part::bytes(file2_content).file_name(file2_name);
425        let form = multipart::Form::new()
426            .part("file1", file1_part)
427            .part("file2", file2_part);
428
429        let mut request = self.http_client.post(validated_url.to_string());
430        if let Some(jwt) = jwt_data.as_ref() {
431            request = request.bearer_auth(&jwt.token);
432        }
433
434        let response = request.multipart(form).send().await?;
435        let result = response.json::<serde_json::Value>().await?;
436
437        println!("✅ Diff analysis complete!");
438        println!("Results: {}", serde_json::to_string_pretty(&result)?);
439
440        Ok(())
441    }
442
443    async fn handle_chat_command(&mut self, message: &str) -> Result<()> {
444        let jwt_data = match self.jwt_store.load_jwt()? {
445            Some(data) => data,
446            None => {
447                println!("❌ Authentication required for chat functionality.");
448                println!();
449                self.show_upgrade_message();
450                return Ok(());
451            }
452        };
453
454        if !jwt_data.features.chat_enabled {
455            println!("❌ Chat feature not available in your current plan.");
456            println!();
457            self.show_upgrade_message();
458            return Ok(());
459        }
460
461        println!("💬 Chat: {}", message);
462
463        let base_url = self.config_store.get_base_url()?;
464        let url = format!("{}/binary/chat", base_url);
465
466        let ssrf_validator = SSRFValidator::new();
467        let validated_url = ssrf_validator.validate_url(&url)?;
468
469        let response = self
470            .http_client
471            .post(validated_url.to_string())
472            .bearer_auth(&jwt_data.token)
473            .json(&json!({ "message": message }))
474            .send()
475            .await?;
476        let result = response.json::<serde_json::Value>().await?;
477
478        println!("✅ Chat response received!");
479        println!("Response: {}", serde_json::to_string_pretty(&result)?);
480
481        Ok(())
482    }
483
484    fn handle_upgrade_command(&mut self) -> Result<()> {
485        if let Some(_jwt_data) = self.jwt_store.load_jwt()? {
486            println!("✅ You are already authenticated!");
487            println!();
488            println!("💡 You can use the CLI to analyze binaries:");
489            println!("  nabla binary analyze /path/to/binary");
490            return Ok(());
491        }
492
493        self.show_upgrade_message();
494        Ok(())
495    }
496
497    fn show_upgrade_message(&self) {
498        let scheduling_url = "https://cal.com/team/atelier-logos/platform-intro";
499
500        println!("🚀 Ready to upgrade to Nabla Pro?");
501        println!();
502        println!("Let's discuss the perfect plan for your security needs:");
503        println!("  • Binary analysis with AI-powered insights");
504        println!("  • Signed attestation and compliance features");
505        println!("  • Custom deployment and enterprise integrations");
506        println!("  • Dedicated support and training");
507        println!();
508
509        #[cfg(feature = "cloud")]
510        {
511            if let Err(e) = webbrowser::open(scheduling_url) {
512                println!("❌ Could not open browser automatically: {}", e);
513                println!("Please visit: {}", scheduling_url);
514            } else {
515                println!("🌐 Opening scheduling page in your browser...");
516                println!("📅 Schedule your demo: {}", scheduling_url);
517            }
518        }
519
520        #[cfg(not(feature = "cloud"))]
521        {
522            println!("📅 Schedule your demo: {}", scheduling_url);
523            println!("💡 Copy and paste this link into your browser to get started.");
524        }
525
526        println!();
527        println!("After our call, you'll receive a token to get started:");
528        println!("  nabla auth --set-jwt <YOUR_TOKEN>");
529    }
530
531    async fn handle_server_command(&self, port: u16) -> Result<()> {
532        println!("🚀 Starting Nabla server on port {}", port);
533        println!("📡 Server will be available at: http://localhost:{}", port);
534        println!("🔐 Endpoints:");
535        println!("  POST /binary/analyze   - Binary analysis");
536        println!("  POST /binary/attest    - Binary attestation (Premium)");
537        println!("  POST /binary/check-cves - CVE checking");
538        println!("  POST /binary/diff      - Binary comparison");
539        println!("  POST /binary/chat      - AI chat (Premium)");
540        println!();
541        println!("💡 Use Ctrl+C to stop the server");
542        println!();
543
544        crate::server::run_server(port).await
545    }
546}