use axum::extract::DefaultBodyLimit;
use axum::{
Router,
http::{Method, header},
routing::post,
};
use base64::Engine;
use dotenvy::dotenv;
use reqwest::Client; use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod binary;
mod cli;
mod config;
mod middleware;
mod routes;
mod ssrf_protection;
mod enterprise;
pub mod server {
pub use crate::run_server;
}
use config::Config;
use middleware::validate_license_jwt;
#[derive(Clone)]
pub struct AppState {
pub config: Config,
pub client: Client,
pub base_url: String,
pub enterprise_features: bool, pub license_jwt_secret: Arc<[u8; 32]>,
}
pub async fn run_server(port: u16) -> anyhow::Result<()> {
dotenv().ok();
let config = Config::from_env()?;
let key_b64 = config.license_signing_key.clone();
let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(key_b64.trim())?;
let secret_array: [u8; 32] = decoded
.try_into()
.map_err(|_| anyhow::anyhow!("LICENSE_SIGNING_KEY must be exactly 32 bytes"))?;
let license_jwt_secret = Arc::new(secret_array);
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
"nabla=debug,tower_http=debug",
))
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.init();
let config = Config::from_env()?;
let base_url =
std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
let client = reqwest::Client::new();
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
let state = AppState {
config: config.clone(),
client,
base_url,
enterprise_features: config.enterprise_features,
license_jwt_secret,
};
let auth_layer = axum::middleware::from_fn_with_state(state.clone(), validate_license_jwt);
let public_routes = Router::new()
.route("/health", axum::routing::get(routes::health_check))
.route("/debug/multipart", post(routes::debug_multipart));
let protected_routes = Router::new()
.route("/binary/analyze", post(routes::upload_and_analyze_binary))
.route("/binary/diff", post(routes::diff_binaries))
.route(
"/binary/attest",
post(enterprise::attestation::attest_binary),
)
.route("/binary/check-cves", post(routes::check_cve))
.route_layer(auth_layer);
let app = Router::new()
.merge(public_routes)
.merge(protected_routes)
.layer(cors)
.layer(DefaultBodyLimit::max(64 * 1024 * 1024))
.with_state(state);
let listener = tokio::net::TcpListener::bind(&format!("0.0.0.0:{}", port)).await?;
tracing::info!("Server starting on port {}", port);
tracing::info!(
"Deployment: {:?}, Enterprise features: {}",
config.deployment_type,
config.enterprise_features
);
axum::serve(listener, app).await?;
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
use clap::Parser;
use cli::{Commands, NablaCli};
#[derive(Parser)]
#[command(name = "nabla")]
#[command(about = "Nabla Binary Analysis & Security Platform")]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long)]
server: bool,
#[arg(long, default_value = "8080")]
port: u16,
}
let cli = Cli::parse();
if cli.server {
return run_server(cli.port).await;
}
match cli.command {
Some(command) => {
let mut nabla_cli = NablaCli::new()?;
nabla_cli.handle_command(command).await
}
None => {
let nabla_cli = NablaCli::new()?;
nabla_cli.show_intro_and_help().await
}
}
}