diff --git a/src/alerts/target.rs b/src/alerts/target.rs index ba700ae2a..abe8792a5 100644 --- a/src/alerts/target.rs +++ b/src/alerts/target.rs @@ -198,48 +198,36 @@ impl Target { pub fn mask(self) -> Value { match self.target { TargetType::Slack(slack_web_hook) => { - let endpoint = slack_web_hook.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; json!({ "name":self.name, "type":"slack", - "endpoint":masked_endpoint, + "endpoint":format!("{}://********", slack_web_hook.endpoint.scheme()), "id":self.id }) } TargetType::Other(other_web_hook) => { - let endpoint = other_web_hook.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; + let safe_headers: HashMap = other_web_hook + .headers + .into_keys() + .map(|k| (k, "********".to_string())) + .collect(); json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, - "headers":other_web_hook.headers, + "endpoint": format!("{}://********", other_web_hook.endpoint.scheme()), + "headers":safe_headers, "skipTlsCheck":other_web_hook.skip_tls_check, "id":self.id }) } TargetType::AlertManager(alert_manager) => { - let endpoint = alert_manager.endpoint.to_string(); - let masked_endpoint = if endpoint.len() > 20 { - format!("{}********", &endpoint[..20]) - } else { - "********".to_string() - }; + let endpoint = format!("{}://********", alert_manager.endpoint.scheme()); if let Some(auth) = alert_manager.auth { let password = "********"; json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, + "endpoint":endpoint, "username":auth.username, "password":password, "skipTlsCheck":alert_manager.skip_tls_check, @@ -249,7 +237,7 @@ impl Target { json!({ "name":self.name, "type":"webhook", - "endpoint":masked_endpoint, + "endpoint":endpoint, "username":Value::Null, "password":Value::Null, "skipTlsCheck":alert_manager.skip_tls_check, @@ -652,3 +640,39 @@ pub struct Auth { username: String, password: String, } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_masked_target_hides_secrets() { + let target = Target { + name: "test-target".into(), + id: Ulid::new(), + tenant: None, + target: TargetType::AlertManager(AlertManager { + endpoint: "https://internal.corp/api".parse().unwrap(), + auth: Some(Auth { + username: "admin".into(), + password: "SuperSecretPassword123".into(), + }), + skip_tls_check: false, + }), + }; + + let masked = target.mask(); + + let masked_str = serde_json::to_string(&masked).unwrap(); + + // These assertions MUST pass, or the build fails + assert!( + !masked_str.contains("SuperSecretPassword123"), + "Password leaked in masked response!" + ); + assert!( + masked_str.contains("********"), + "Expected redaction marker not found!" + ); + } +} diff --git a/src/handlers/http/targets.rs b/src/handlers/http/targets.rs index 919a40df5..db91c1e3d 100644 --- a/src/handlers/http/targets.rs +++ b/src/handlers/http/targets.rs @@ -42,8 +42,7 @@ pub async fn post( // add to the map TARGETS.update(target.clone()).await?; - // Ok(web::Json(target.mask())) - Ok(web::Json(target)) + Ok(web::Json(target.mask())) } // GET /targets @@ -54,7 +53,7 @@ pub async fn list(req: HttpRequest) -> Result { .list(&tenant_id) .await? .into_iter() - // .map(|t| t.mask()) + .map(|t| t.mask()) .collect_vec(); Ok(web::Json(list)) @@ -66,8 +65,7 @@ pub async fn get(req: HttpRequest, target_id: Path) -> Result) -> Result