From 09d26d62f86195572338ca989a5902618bb99223 Mon Sep 17 00:00:00 2001 From: thibaud-lclr Date: Fri, 17 Apr 2026 16:31:31 +0200 Subject: [PATCH] fix(graylog): use basic auth with base64 token fallback --- src-tauri/src/services/graylog_client.rs | 79 +++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/services/graylog_client.rs b/src-tauri/src/services/graylog_client.rs index a65d4e7..b0a0ea7 100644 --- a/src-tauri/src/services/graylog_client.rs +++ b/src-tauri/src/services/graylog_client.rs @@ -1,4 +1,5 @@ use crate::services::graylog_scoring::GraylogEvent; +use base64::{engine::general_purpose::STANDARD, Engine}; use serde_json::Value; use std::time::Instant; use tokio::time::{sleep, Duration}; @@ -18,7 +19,11 @@ impl GraylogClient { } } - async fn send_get(&self, url: &str) -> Result { + async fn send_get_with_auth( + &self, + url: &str, + auth_token: &str, + ) -> Result { const MAX_ATTEMPTS: u32 = 3; const BASE_DELAY_MS: u64 = 500; @@ -32,7 +37,7 @@ impl GraylogClient { let response = self .http .get(url) - .header("Authorization", format!("Bearer {}", self.token)) + .basic_auth(auth_token, Some("token")) .header("X-Requested-By", "orchai") .send() .await; @@ -88,6 +93,25 @@ impl GraylogClient { Err("graylog request failed after retries".to_string()) } + async fn send_get(&self, url: &str) -> Result { + let auth_tokens = build_auth_tokens(&self.token); + + for (index, auth_token) in auth_tokens.iter().enumerate() { + let response = self.send_get_with_auth(url, auth_token).await?; + if response.status() == reqwest::StatusCode::UNAUTHORIZED + && index + 1 < auth_tokens.len() + { + eprintln!( + "[graylog] auth token rejected (401), retrying with base64-decoded token" + ); + continue; + } + return Ok(response); + } + + Err("graylog request failed after auth fallback".to_string()) + } + pub async fn test_connection(&self) -> Result<(), String> { let url = format!("{}/api/system", self.base_url); let resp = self.send_get(&url).await?; @@ -95,7 +119,10 @@ impl GraylogClient { if resp.status().is_success() { Ok(()) } else { - Err(format!("graylog connection test failed: HTTP {}", resp.status())) + Err(format!( + "graylog connection test failed: HTTP {}", + resp.status() + )) } } @@ -132,6 +159,33 @@ impl GraylogClient { } } +fn decode_token_candidate(token: &str) -> Option { + if token.is_empty() { + return None; + } + let decoded_bytes = STANDARD.decode(token).ok()?; + let decoded = String::from_utf8(decoded_bytes).ok()?; + let decoded = decoded.trim(); + if decoded.is_empty() { + return None; + } + if !decoded.chars().all(|c| c.is_ascii_alphanumeric()) { + return None; + } + Some(decoded.to_string()) +} + +fn build_auth_tokens(token: &str) -> Vec { + let token = token.trim().to_string(); + let mut candidates = vec![token.clone()]; + if let Some(decoded) = decode_token_candidate(&token) { + if decoded != token { + candidates.push(decoded); + } + } + candidates +} + fn level_to_string(value: &Value) -> String { match value { Value::String(s) => s.to_string(), @@ -255,4 +309,23 @@ mod tests { let events = parse_search_response(&payload); assert!(events.is_empty()); } + + #[test] + fn test_new_decodes_base64_token_for_stackhero_format() { + let encoded = "OTk2c2l2aGY1Z243Zm9xNzh2ajVrbmlkZWQ4bW9idHZrZ3Nhb2lvNDRrbTY3MnZkaWc2"; + let candidates = build_auth_tokens(encoded); + assert_eq!(candidates.len(), 2); + assert_eq!(candidates[0], encoded); + assert_eq!( + candidates[1], + "996sivhf5gn7foq78vj5knided8mobtvkgsaoio44km672vdig6" + ); + } + + #[test] + fn test_build_auth_tokens_keeps_raw_when_not_base64() { + let raw = "996sivhf5gn7foq78vj5knided8mobtvkgsaoio44km672vdig6"; + let candidates = build_auth_tokens(raw); + assert_eq!(candidates, vec![raw.to_string()]); + } }