From 987888c29f2c067ef20869efbdacc2f4bc319ef0 Mon Sep 17 00:00:00 2001 From: Ibrahim Rahhal Date: Sun, 24 Aug 2025 12:26:54 +0300 Subject: [PATCH 1/3] New login flow PoC --- Cargo.lock | 168 ++++++++++++++++++- Cargo.toml | 7 + src/authorize.rs | 421 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 48 ++++-- src/utils/api.rs | 28 ++++ 5 files changed, 659 insertions(+), 13 deletions(-) create mode 100644 src/authorize.rs diff --git a/Cargo.lock b/Cargo.lock index dfe2dc2..7c46d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" @@ -317,14 +323,18 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "corgea" -version = "1.6.2" +version = "1.6.5" dependencies = [ "chrono", "clap", "dirs", "git2", "globset", + "http-body-util", + "hyper", + "hyper-util", "ignore", + "open", "quick-xml", "regex", "reqwest", @@ -333,7 +343,10 @@ dependencies = [ "serde_json", "tempfile", "termcolor", + "tokio", "toml", + "url", + "urlencoding", "uuid", "which", "zip", @@ -648,6 +661,25 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.15.2" @@ -718,6 +750,12 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.6.0" @@ -727,9 +765,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -978,6 +1018,25 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1061,6 +1120,16 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "lockfree-object-pool" version = "0.1.6" @@ -1166,6 +1235,17 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl-probe" version = "0.1.6" @@ -1178,6 +1258,35 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1336,6 +1445,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -1546,6 +1664,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.2.0" @@ -1639,6 +1763,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1836,11 +1969,25 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-rustls" version = "0.26.1" @@ -1851,6 +1998,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.19" @@ -1972,6 +2132,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf16_iter" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 680e41e..9d6d336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,10 @@ termcolor = "1.1" git2 = { version = "0.20.0", default-features = false } regex = "1" chrono = "0.4" +tokio = { version = "1.0", features = ["full"] } +hyper = { version = "1.0", features = ["full"] } +hyper-util = { version = "0.1", features = ["full"] } +http-body-util = "0.1" +url = "2.5" +open = "5.0" +urlencoding = "2.1" diff --git a/src/authorize.rs b/src/authorize.rs new file mode 100644 index 0000000..197eeb9 --- /dev/null +++ b/src/authorize.rs @@ -0,0 +1,421 @@ +use crate::{config::Config, utils::{terminal, api}}; +use hyper::body::Incoming; +use hyper::service::service_fn; +use hyper::{Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use http_body_util::Full; +use hyper::body::Bytes; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use tokio::net::TcpListener; + + +const DEFAULT_PORT: u16 = 9876; + +pub fn run(scope: Option, url: Option) -> Result<(), Box> { + // Build the authorization URL + let base_domain = match (scope, url) { + // If scope is provided, use it (takes precedence) + (Some(ref s), _) if !s.is_empty() => format!("https://{}.corgea.app", s), + // If URL is provided but no scope, use the URL + (_, Some(ref u)) if !u.is_empty() => u.clone(), + // Default fallback + _ => "https://www.corgea.app".to_string(), + }; + + // Find available port starting from default + let port = find_available_port(DEFAULT_PORT)?; + let callback_url = format!("http://localhost:{}", port); + let auth_url = format!("{}/authorize?callback={}", base_domain, + urlencoding::encode(&callback_url)); + + println!("Opening browser to authorize Corgea CLI..."); + println!("Authorization URL: {}", auth_url); + + // Open browser + if let Err(e) = open::that(&auth_url) { + eprintln!("Failed to open browser automatically: {}", e); + println!("Please manually open the following URL in your browser:"); + println!("{}", auth_url); + } + + // Set up shared state for the authorization code + let auth_code = Arc::new(Mutex::new(None::)); + let auth_code_clone = auth_code.clone(); + + // Set up loading message + let stop_signal = Arc::new(Mutex::new(false)); + let stop_signal_clone = stop_signal.clone(); + + // Start loading spinner in a separate thread + let loading_handle = thread::spawn(move || { + terminal::show_loading_message("Waiting for authorization...", stop_signal_clone); + }); + + // Start the HTTP server to listen for the callback + let rt = tokio::runtime::Runtime::new()?; + let result = rt.block_on(async { + start_callback_server(port, auth_code_clone).await + }); + + // Stop the loading spinner + *stop_signal.lock().unwrap() = true; + loading_handle.join().unwrap(); + + match result { + Ok(code) => { + + // Exchange the code for a user token + let user_token = api::exchange_code_for_token(&base_domain, &code)?; + + // Save the user token to config + let mut config = Config::load().expect("Failed to load config"); + config.set_token(user_token).expect("Failed to save user token"); + config.set_url(base_domain).expect("Failed to save URL"); + + println!("\r🎉 Successfully authenticated to Corgea!"); + println!("You can now use other Corgea CLI commands."); + + Ok(()) + } + Err(e) => { + eprintln!("\r❌ Authorization failed: {}", e); + Err(e) + } + } +} + +fn find_available_port(start_port: u16) -> Result> { + // Try a more reliable approach - start from a higher range that's less likely to be used + let search_ranges = vec![ + (start_port, start_port + 50), + (9000, 9100), + (8000, 8100), + (7000, 7100), + ]; + + for (range_start, range_end) in search_ranges { + for port in range_start..range_end { + if port_is_available(port) { + return Ok(port); + } + } + } + + Err("No available ports found after checking multiple ranges".into()) +} + +fn port_is_available(port: u16) -> bool { + match std::net::TcpListener::bind(format!("127.0.0.1:{}", port)) { + Ok(listener) => { + // Successfully bound - port is available + // The listener will be dropped here, freeing the port + drop(listener); + true + } + Err(_) => { + // Port is in use or binding failed + false + } + } +} + +async fn start_callback_server( + port: u16, + auth_code: Arc>>, +) -> Result> { + let addr = format!("127.0.0.1:{}", port); + let listener = match TcpListener::bind(&addr).await { + Ok(listener) => { + listener + } + Err(e) => { + return Err(format!("Failed to bind to {}: {}", addr, e).into()); + } + }; + + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + let auth_code_clone = auth_code.clone(); + + let service = service_fn(move |req| { + handle_callback(req, auth_code_clone.clone()) + }); + + tokio::task::spawn(async move { + if let Err(err) = hyper::server::conn::http1::Builder::new() + .serve_connection(io, service) + .await + { + eprintln!("Error serving connection: {:?}", err); + } + }); + + // Check if we got the code + if let Ok(code_guard) = auth_code.lock() { + if let Some(code) = code_guard.as_ref() { + return Ok(code.clone()); + } + } + + // Add a small delay to prevent busy waiting + tokio::time::sleep(Duration::from_millis(100)).await; + } +} + +async fn handle_callback( + req: Request, + auth_code: Arc>>, +) -> Result>, hyper::Error> { + let uri = req.uri(); + + // Parse query parameters + if let Some(query) = uri.query() { + let params = parse_query_params(query); + + if let Some(code) = params.get("code") { + // Store the authorization code + if let Ok(mut code_guard) = auth_code.lock() { + *code_guard = Some(code.clone()); + } + + // Return success page + let success_html = r#" + + + + Corgea CLI - Authorization Successful + + + + + + +
+

Authorization Successful!

+
Your Corgea CLI has been authorized.
+
You can now close this browser tab and return to your terminal.
+
+ + + + "#; + + return Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/html") + .body(Full::new(Bytes::from(success_html))) + .unwrap()); + } + + if let Some(error) = params.get("error") { + let default_error = "Unknown error occurred".to_string(); + let error_description = params.get("error_description") + .unwrap_or(&default_error); + + let error_html = format!(r#" + + + + Corgea CLI - Authorization Failed + + + + + + +
+
+

Authorization Failed

+
Error: {}
+
{}
+
Please return to your terminal and try again.
+
+ + + "#, error, error_description); + + return Ok(Response::builder() + .status(StatusCode::BAD_REQUEST) + .header("Content-Type", "text/html") + .body(Full::new(Bytes::from(error_html))) + .unwrap()); + } + } + + // Default response for other requests + let response_html = r#" + + + + Corgea CLI - Waiting for Authorization + + + + + + +
+

Waiting for Authorization...

+

Please complete the authorization process in the main browser window.

+
+ + + "#; + + Ok(Response::builder() + .status(StatusCode::OK) + .header("Content-Type", "text/html") + .body(Full::new(Bytes::from(response_html))) + .unwrap()) +} + +fn parse_query_params(query: &str) -> HashMap { + query + .split('&') + .filter_map(|param| { + let mut parts = param.splitn(2, '='); + match (parts.next(), parts.next()) { + (Some(key), Some(value)) => { + Some(( + urlencoding::decode(key).ok()?.into_owned(), + urlencoding::decode(value).ok()?.into_owned(), + )) + } + _ => None, + } + }) + .collect() +} + + diff --git a/src/main.rs b/src/main.rs index d85103a..fd0f6b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod inspect; mod cicd; mod log; mod setup_hooks; +mod authorize; mod scanners { pub mod fortify; pub mod blast; @@ -37,12 +38,15 @@ struct Cli { enum Commands { /// Authenticate to Corgea Login { - token: String, + #[arg(help = "API token (if not provided, will use OAuth flow)")] + token: Option, #[arg(long, help = "The url of the corgea instance to use. defaults to https://www.corgea.app")] url: Option, - - }, + + #[arg(long, help = "Scope to use for custom domain (e.g., 'ikea' for ikea.corgea.app). Only used with OAuth flow")] + scope: Option, + }, /// Upload a scan report to Corgea via STDIN or a file Upload { /// Option path to JSON report to upload @@ -178,17 +182,37 @@ fn main() { } } match &cli.command { - Some(Commands::Login { token, url }) => { - match utils::api::verify_token(token, url.as_deref().unwrap_or(corgea_config.get_url().as_str())) { - Ok(true) => { - corgea_config.set_token(token.clone()).expect("Failed to set token"); - if let Some(url) = url { - corgea_config.set_url(url.clone()).expect("Failed to set url"); + Some(Commands::Login { token, url, scope }) => { + match token { + // Token provided - use traditional token-based login + Some(token) => { + match utils::api::verify_token(token, url.as_deref().unwrap_or(corgea_config.get_url().as_str())) { + Ok(true) => { + corgea_config.set_token(token.clone()).expect("Failed to set token"); + if let Some(url) = url { + corgea_config.set_url(url.clone()).expect("Failed to set url"); + } + println!("Successfully authenticated to Corgea.") + } + Ok(false) => println!("Invalid token provided."), + Err(e) => eprintln!("Error occurred: {}", e), + } + } + // No token provided - use OAuth flow + None => { + // If URL is provided with OAuth, show a warning since OAuth determines the URL + if url.is_some() && scope.is_some() { + eprintln!("Warning: --url option is ignored when using OAuth flow with --scope. The scope determines the domain."); + } + + match authorize::run(scope.clone(), url.clone()) { + Ok(()) => {}, + Err(e) => { + eprintln!("Authorization failed: {}", e); + std::process::exit(1); + } } - println!("Successfully authenticated to Corgea.") } - Ok(false) => println!("Invalid token provided."), - Err(e) => eprintln!("Error occurred: {}", e), } } Some(Commands::Upload { report }) => { diff --git a/src/utils/api.rs b/src/utils/api.rs index 466d6a4..627df49 100644 --- a/src/utils/api.rs +++ b/src/utils/api.rs @@ -406,6 +406,34 @@ pub fn query_scan_list( } +pub fn exchange_code_for_token( + base_url: &str, + code: &str, +) -> Result> { + let client = reqwest::blocking::Client::new(); + let exchange_url = format!("{}{}/authorize", base_url, API_BASE); + + let response = client + .get(&exchange_url) + .query(&[("code", code)]) + .send()?; + + if response.status().is_success() { + let response_json: HashMap = response.json()?; + + if let Some(user_token) = response_json.get("user_token") { + if let Some(user_token_str) = user_token.as_str() { + return Ok(user_token_str.to_string()); + } + } + + Err("User token not found in response".into()) + } else { + let error_text = response.text().unwrap_or_else(|_| "Unknown error".to_string()); + Err(format!("Failed to exchange code for user token: {}", error_text).into()) + } +} + pub fn verify_token(token: &str, corgea_url: &str) -> Result> { let url = format!("{}{}/verify", corgea_url, API_BASE); let client = reqwest::blocking::Client::new(); From 8f88f68cf8497840647d7fbcabbb1e3d58da7a63 Mon Sep 17 00:00:00 2001 From: Ibrahim Rahhal Date: Sat, 30 Aug 2025 17:57:16 +0300 Subject: [PATCH 2/3] Updating design template --- src/authorize.rs | 166 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 135 insertions(+), 31 deletions(-) diff --git a/src/authorize.rs b/src/authorize.rs index 197eeb9..4d0475e 100644 --- a/src/authorize.rs +++ b/src/authorize.rs @@ -191,60 +191,164 @@ async fn handle_callback( +
-

Authorization Successful!

-
Your Corgea CLI has been authorized.
-
You can now close this browser tab and return to your terminal.
+
+

Successfully Signed In!

+
Your CLI is now authenticated with Corgea
+ +
+

Next Steps

+
+
+
+
+
Return to your CLI
+
Go back to your terminal and start running security scans on your codebase
+
+
+
+
+
+
Run Your First Scan
+
Use the Corgea CLI commands to analyze your code for security vulnerabilities
+
+
+
+
+ + +
From a3513b7cf13aeee10d083be7bd55828cb3853d23 Mon Sep 17 00:00:00 2001 From: Ibrahim Rahhal Date: Fri, 19 Sep 2025 14:13:18 +0300 Subject: [PATCH 3/3] Improvement --- src/main.rs | 28 ++++++++++++++++++---------- src/utils/generic.rs | 7 +++++++ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index fd0f6b9..d98ba95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -183,24 +183,32 @@ fn main() { } match &cli.command { Some(Commands::Login { token, url, scope }) => { - match token { - // Token provided - use traditional token-based login - Some(token) => { - match utils::api::verify_token(token, url.as_deref().unwrap_or(corgea_config.get_url().as_str())) { + let effective_token = token.clone().or_else(|| utils::generic::get_env_var_if_exists("CORGEA_TOKEN")); + + match effective_token { + Some(token_value) => { + let token_source = if token.is_some() { "parameter" } else { "CORGEA_TOKEN environment variable" }; + match utils::api::verify_token(&token_value, url.as_deref().unwrap_or(corgea_config.get_url().as_str())) { Ok(true) => { - corgea_config.set_token(token.clone()).expect("Failed to set token"); + corgea_config.set_token(token_value.clone()).expect("Failed to set token"); if let Some(url) = url { corgea_config.set_url(url.clone()).expect("Failed to set url"); } - println!("Successfully authenticated to Corgea.") + println!("Successfully authenticated to Corgea using token from {}.", token_source) } - Ok(false) => println!("Invalid token provided."), - Err(e) => eprintln!("Error occurred: {}", e), + Ok(false) => println!("Invalid token provided from {}.", token_source), + Err(e) => { + if e.to_string().contains("401") { + println!("Invalid token provided from {}.", token_source); + std::process::exit(1); + } + eprintln!("Error occurred: {}", e); + std::process::exit(1); + }, } } - // No token provided - use OAuth flow + // No token available - use OAuth flow None => { - // If URL is provided with OAuth, show a warning since OAuth determines the URL if url.is_some() && scope.is_some() { eprintln!("Warning: --url option is ignored when using OAuth flow with --scope. The scope determines the domain."); } diff --git a/src/utils/generic.rs b/src/utils/generic.rs index c8d6ce1..0499585 100644 --- a/src/utils/generic.rs +++ b/src/utils/generic.rs @@ -157,6 +157,13 @@ pub fn get_current_working_directory() -> Option { .and_then(|path| path.file_name().map(|name| name.to_string_lossy().to_string())) } +pub fn get_env_var_if_exists(var_name: &str) -> Option { + match env::var(var_name) { + Ok(value) if !value.trim().is_empty() => Some(value), + _ => None, + } +} + pub fn get_repo_info(dir: &str) -> Result, git2::Error> { let repo = match Repository::open(Path::new(dir)) { Ok(repo) => repo,