Skip to content

Commit fd7d7f2

Browse files
committed
This patch re-enables masking with a few backend changes
- Add mask_url() using url based parsing - Apply consistent masking across Slack, Webhook, and AlertManager types. - Add strict unit tests enforcing that secrets never hit the serializer.
1 parent 595af02 commit fd7d7f2

2 files changed

Lines changed: 66 additions & 32 deletions

File tree

src/alerts/target.rs

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -195,51 +195,53 @@ pub struct Target {
195195
}
196196

197197
impl Target {
198+
/// Sanitizes a URL by obliterating the trailing path segment.
199+
fn mask_url(url: &Url) -> String {
200+
let mut base = url.clone();
201+
if let Ok(mut segments) = base.path_segments_mut() {
202+
segments.clear();
203+
}
204+
base.set_query(None);
205+
base.set_fragment(None);
206+
base.set_path("/********");
207+
base.to_string()
208+
}
209+
198210
pub fn mask(self) -> Value {
199211
match self.target {
200212
TargetType::Slack(slack_web_hook) => {
201-
let endpoint = slack_web_hook.endpoint.to_string();
202-
let masked_endpoint = if endpoint.len() > 20 {
203-
format!("{}********", &endpoint[..20])
204-
} else {
205-
"********".to_string()
206-
};
213+
let endpoint = Self::mask_url(&slack_web_hook.endpoint);
207214
json!({
208215
"name":self.name,
209216
"type":"slack",
210-
"endpoint":masked_endpoint,
217+
"endpoint": endpoint,
211218
"id":self.id
212219
})
213220
}
214221
TargetType::Other(other_web_hook) => {
215-
let endpoint = other_web_hook.endpoint.to_string();
216-
let masked_endpoint = if endpoint.len() > 20 {
217-
format!("{}********", &endpoint[..20])
218-
} else {
219-
"********".to_string()
220-
};
222+
let endpoint = Self::mask_url(&other_web_hook.endpoint);
223+
let safe_headers: HashMap<String, String> = other_web_hook
224+
.headers
225+
.into_iter()
226+
.map(|(k, _v)| (k, "********".to_string()))
227+
.collect();
221228
json!({
222229
"name":self.name,
223230
"type":"webhook",
224-
"endpoint":masked_endpoint,
225-
"headers":other_web_hook.headers,
231+
"endpoint":endpoint,
232+
"headers":safe_headers,
226233
"skipTlsCheck":other_web_hook.skip_tls_check,
227234
"id":self.id
228235
})
229236
}
230237
TargetType::AlertManager(alert_manager) => {
231-
let endpoint = alert_manager.endpoint.to_string();
232-
let masked_endpoint = if endpoint.len() > 20 {
233-
format!("{}********", &endpoint[..20])
234-
} else {
235-
"********".to_string()
236-
};
238+
let endpoint = Self::mask_url(&alert_manager.endpoint);
237239
if let Some(auth) = alert_manager.auth {
238240
let password = "********";
239241
json!({
240242
"name":self.name,
241243
"type":"webhook",
242-
"endpoint":masked_endpoint,
244+
"endpoint":endpoint,
243245
"username":auth.username,
244246
"password":password,
245247
"skipTlsCheck":alert_manager.skip_tls_check,
@@ -249,7 +251,7 @@ impl Target {
249251
json!({
250252
"name":self.name,
251253
"type":"webhook",
252-
"endpoint":masked_endpoint,
254+
"endpoint":endpoint,
253255
"username":Value::Null,
254256
"password":Value::Null,
255257
"skipTlsCheck":alert_manager.skip_tls_check,
@@ -652,3 +654,39 @@ pub struct Auth {
652654
username: String,
653655
password: String,
654656
}
657+
658+
#[cfg(test)]
659+
mod tests {
660+
use super::*;
661+
662+
#[test]
663+
fn test_masked_target_hides_secrets() {
664+
let target = Target {
665+
name: "test-target".into(),
666+
id: Ulid::new(),
667+
tenant: None,
668+
target: TargetType::AlertManager(AlertManager {
669+
endpoint: "https://internal.corp/api".parse().unwrap(),
670+
auth: Some(Auth {
671+
username: "admin".into(),
672+
password: "SuperSecretPassword123".into(),
673+
}),
674+
skip_tls_check: false,
675+
}),
676+
};
677+
678+
let masked = target.mask();
679+
680+
let masked_str = serde_json::to_string(&masked).unwrap();
681+
682+
// These assertions MUST pass, or the build fails
683+
assert!(
684+
!masked_str.contains("SuperSecretPassword123"),
685+
"Password leaked in masked response!"
686+
);
687+
assert!(
688+
masked_str.contains("********"),
689+
"Expected redaction marker not found!"
690+
);
691+
}
692+
}

src/handlers/http/targets.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@ pub async fn post(
4242
// add to the map
4343
TARGETS.update(target.clone()).await?;
4444

45-
// Ok(web::Json(target.mask()))
46-
Ok(web::Json(target))
45+
Ok(web::Json(target.mask()))
4746
}
4847

4948
// GET /targets
@@ -54,7 +53,7 @@ pub async fn list(req: HttpRequest) -> Result<impl Responder, AlertError> {
5453
.list(&tenant_id)
5554
.await?
5655
.into_iter()
57-
// .map(|t| t.mask())
56+
.map(|t| t.mask())
5857
.collect_vec();
5958

6059
Ok(web::Json(list))
@@ -66,8 +65,7 @@ pub async fn get(req: HttpRequest, target_id: Path<Ulid>) -> Result<impl Respond
6665
let tenant_id = get_tenant_id_from_request(&req);
6766
let target = TARGETS.get_target_by_id(&target_id, &tenant_id).await?;
6867

69-
// Ok(web::Json(target.mask()))
70-
Ok(web::Json(target))
68+
Ok(web::Json(target.mask()))
7169
}
7270

7371
// PUT /targets/{target_id}
@@ -95,8 +93,7 @@ pub async fn update(
9593
// add to the map
9694
TARGETS.update(target.clone()).await?;
9795

98-
// Ok(web::Json(target.mask()))
99-
Ok(web::Json(target))
96+
Ok(web::Json(target.mask()))
10097
}
10198

10299
// DELETE /targets/{target_id}
@@ -105,6 +102,5 @@ pub async fn delete(req: HttpRequest, target_id: Path<Ulid>) -> Result<impl Resp
105102
let tenant_id = get_tenant_id_from_request(&req);
106103
let target = TARGETS.delete(&target_id, &tenant_id).await?;
107104

108-
// Ok(web::Json(target.mask()))
109-
Ok(web::Json(target))
105+
Ok(web::Json(target.mask()))
110106
}

0 commit comments

Comments
 (0)