fix(graylog): use basic auth with base64 token fallback

This commit is contained in:
thibaud-lclr 2026-04-17 16:31:31 +02:00
parent d6834cf648
commit 09d26d62f8

View file

@ -1,4 +1,5 @@
use crate::services::graylog_scoring::GraylogEvent; use crate::services::graylog_scoring::GraylogEvent;
use base64::{engine::general_purpose::STANDARD, Engine};
use serde_json::Value; use serde_json::Value;
use std::time::Instant; use std::time::Instant;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@ -18,7 +19,11 @@ impl GraylogClient {
} }
} }
async fn send_get(&self, url: &str) -> Result<reqwest::Response, String> { async fn send_get_with_auth(
&self,
url: &str,
auth_token: &str,
) -> Result<reqwest::Response, String> {
const MAX_ATTEMPTS: u32 = 3; const MAX_ATTEMPTS: u32 = 3;
const BASE_DELAY_MS: u64 = 500; const BASE_DELAY_MS: u64 = 500;
@ -32,7 +37,7 @@ impl GraylogClient {
let response = self let response = self
.http .http
.get(url) .get(url)
.header("Authorization", format!("Bearer {}", self.token)) .basic_auth(auth_token, Some("token"))
.header("X-Requested-By", "orchai") .header("X-Requested-By", "orchai")
.send() .send()
.await; .await;
@ -88,6 +93,25 @@ impl GraylogClient {
Err("graylog request failed after retries".to_string()) Err("graylog request failed after retries".to_string())
} }
async fn send_get(&self, url: &str) -> Result<reqwest::Response, String> {
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> { pub async fn test_connection(&self) -> Result<(), String> {
let url = format!("{}/api/system", self.base_url); let url = format!("{}/api/system", self.base_url);
let resp = self.send_get(&url).await?; let resp = self.send_get(&url).await?;
@ -95,7 +119,10 @@ impl GraylogClient {
if resp.status().is_success() { if resp.status().is_success() {
Ok(()) Ok(())
} else { } 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<String> {
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<String> {
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 { fn level_to_string(value: &Value) -> String {
match value { match value {
Value::String(s) => s.to_string(), Value::String(s) => s.to_string(),
@ -255,4 +309,23 @@ mod tests {
let events = parse_search_response(&payload); let events = parse_search_response(&payload);
assert!(events.is_empty()); 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()]);
}
} }