@@ -195,51 +195,53 @@ pub struct Target {
195195}
196196
197197impl 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+ }
0 commit comments