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; fn validate_file_path(file_path: &str) -> Result<PathBuf> {
20 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 if !path.exists() {
31 return Err(anyhow::anyhow!("File not found: {}", clean_path));
32 }
33
34 let canonical_path = path
36 .canonicalize()
37 .map_err(|e| anyhow::anyhow!("Invalid file path '{}': {}", clean_path, e))?;
38
39 let current_dir = std::env::current_dir()
41 .map_err(|e| anyhow::anyhow!("Cannot determine current directory: {}", e))?;
42
43 if !canonical_path.starts_with(¤t_dir) {
45 return Err(anyhow::anyhow!(
46 "Access denied: file '{}' is outside the current working directory",
47 clean_path
48 ));
49 }
50
51 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 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 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}