@@ -182,6 +182,9 @@ pub fn render_globals(
182182 }
183183
184184 let sql_compat = to_sql_compatible_json ( & rendered) ;
185+ if global_var. protected {
186+ crate :: core:: secrets:: register_secret ( & sql_compat) ;
187+ }
185188 debug ! (
186189 "Setting global variable [{}] to {}" ,
187190 global_var. name, sql_compat
@@ -210,6 +213,9 @@ pub fn render_properties(
210213 if let Some ( ref value) = prop. value {
211214 let rendered = render_value ( engine, value, & resource_context) ;
212215 let sql_compat = to_sql_compatible_json ( & rendered) ;
216+ if prop. protected {
217+ crate :: core:: secrets:: register_secret ( & sql_compat) ;
218+ }
213219 debug ! ( "Setting property [{}] to {}" , prop. name, sql_compat) ;
214220 prop_context. insert ( prop. name . clone ( ) , sql_compat. clone ( ) ) ;
215221 resource_context. insert ( prop. name . clone ( ) , sql_compat) ;
@@ -219,6 +225,9 @@ pub fn render_properties(
219225 if let Some ( env_val) = values. get ( stack_env) {
220226 let rendered = render_value ( engine, & env_val. value , & resource_context) ;
221227 let sql_compat = to_sql_compatible_json ( & rendered) ;
228+ if prop. protected {
229+ crate :: core:: secrets:: register_secret ( & sql_compat) ;
230+ }
222231 debug ! (
223232 "Setting property [{}] using env-specific value to {}" ,
224233 prop. name, sql_compat
@@ -293,6 +302,9 @@ pub fn render_properties(
293302
294303 if let Some ( merged_val) = base_value {
295304 let processed = serde_json:: to_string ( & merged_val) . unwrap_or_default ( ) ;
305+ if prop. protected {
306+ crate :: core:: secrets:: register_secret ( & processed) ;
307+ }
296308 prop_context. insert ( prop. name . clone ( ) , processed. clone ( ) ) ;
297309 resource_context. insert ( prop. name . clone ( ) , processed) ;
298310 }
@@ -437,7 +449,7 @@ pub fn is_json(s: &str) -> bool {
437449#[ cfg( test) ]
438450mod tests {
439451 use super :: * ;
440- use crate :: resource:: manifest:: { Property , Resource } ;
452+ use crate :: resource:: manifest:: { Property , PropertyValue , Resource } ;
441453
442454 /// Helper to create a minimal Resource for testing.
443455 fn make_resource ( name : & str , props : Vec < Property > ) -> Resource {
@@ -466,6 +478,7 @@ mod tests {
466478 values : None ,
467479 description : String :: new ( ) ,
468480 merge : None ,
481+ protected : false ,
469482 }
470483 }
471484
@@ -654,4 +667,115 @@ mod tests {
654667
655668 assert_eq ! ( ctx. get( "client_token" ) . unwrap( ) , token) ;
656669 }
670+
671+ // ------------------------------------------------------------------
672+ // protected (secret) value tests
673+ // ------------------------------------------------------------------
674+
675+ #[ test]
676+ fn test_protected_prop_registered_for_redaction ( ) {
677+ let engine = TemplateEngine :: new ( ) ;
678+ let global_context = HashMap :: new ( ) ;
679+
680+ let mut prop = make_prop ( "master_user_password" , "Cfg-Prop-S3cret-Value-1" ) ;
681+ prop. protected = true ;
682+
683+ let ctx = render_properties ( & engine, & [ prop] , & global_context, "dev" ) ;
684+
685+ // Value is stored unmasked in the context (real queries need it)
686+ assert_eq ! (
687+ ctx. get( "master_user_password" ) . unwrap( ) ,
688+ "Cfg-Prop-S3cret-Value-1"
689+ ) ;
690+ // But the log scrubber masks it wherever it appears
691+ let redacted =
692+ crate :: core:: secrets:: redact ( "INSERT ... SELECT 'Cfg-Prop-S3cret-Value-1', ..." ) ;
693+ assert ! (
694+ !redacted. contains( "Cfg-Prop-S3cret-Value-1" ) ,
695+ "protected prop value leaked: {}" ,
696+ redacted
697+ ) ;
698+ }
699+
700+ #[ test]
701+ fn test_protected_prop_env_specific_value_registered_for_redaction ( ) {
702+ let engine = TemplateEngine :: new ( ) ;
703+ let global_context = HashMap :: new ( ) ;
704+
705+ let mut values = HashMap :: new ( ) ;
706+ values. insert (
707+ "dev" . to_string ( ) ,
708+ PropertyValue {
709+ value : serde_yaml:: Value :: String ( "Cfg-EnvProp-S3cret-Value-2" . to_string ( ) ) ,
710+ } ,
711+ ) ;
712+ let prop = Property {
713+ name : "api_key" . to_string ( ) ,
714+ value : None ,
715+ values : Some ( values) ,
716+ description : String :: new ( ) ,
717+ merge : None ,
718+ protected : true ,
719+ } ;
720+
721+ let ctx = render_properties ( & engine, & [ prop] , & global_context, "dev" ) ;
722+
723+ assert_eq ! ( ctx. get( "api_key" ) . unwrap( ) , "Cfg-EnvProp-S3cret-Value-2" ) ;
724+ let redacted = crate :: core:: secrets:: redact ( "key = 'Cfg-EnvProp-S3cret-Value-2'" ) ;
725+ assert ! ( !redacted. contains( "Cfg-EnvProp-S3cret-Value-2" ) ) ;
726+ }
727+
728+ #[ test]
729+ fn test_protected_global_registered_for_redaction ( ) {
730+ let engine = TemplateEngine :: new ( ) ;
731+ let mut vars = HashMap :: new ( ) ;
732+ vars. insert (
733+ "DB_PASSWORD" . to_string ( ) ,
734+ "Cfg-Global-S3cret-Value-3" . to_string ( ) ,
735+ ) ;
736+
737+ let manifest: Manifest = serde_yaml:: from_str (
738+ r#"
739+ version: 1
740+ name: test-stack
741+ providers:
742+ - aws
743+ globals:
744+ - name: db_password
745+ value: "{{ DB_PASSWORD }}"
746+ protected: true
747+ - name: region
748+ value: us-east-1
749+ "# ,
750+ )
751+ . unwrap ( ) ;
752+
753+ let ctx = render_globals ( & engine, & vars, & manifest, "dev" , "test-stack" ) ;
754+
755+ // Stored unmasked
756+ assert_eq ! ( ctx. get( "db_password" ) . unwrap( ) , "Cfg-Global-S3cret-Value-3" ) ;
757+ // Masked in log output
758+ let redacted = crate :: core:: secrets:: redact ( "password = 'Cfg-Global-S3cret-Value-3'" ) ;
759+ assert ! ( !redacted. contains( "Cfg-Global-S3cret-Value-3" ) ) ;
760+ // Non-protected global is not masked
761+ let not_redacted = crate :: core:: secrets:: redact ( "region = 'us-east-1'" ) ;
762+ assert ! ( not_redacted. contains( "us-east-1" ) ) ;
763+ }
764+
765+ #[ test]
766+ fn test_unprotected_prop_not_registered ( ) {
767+ let engine = TemplateEngine :: new ( ) ;
768+ let global_context = HashMap :: new ( ) ;
769+
770+ let prop = make_prop ( "instance_class" , "Cfg-Plain-Value-Not-Secret-4" ) ;
771+
772+ let ctx = render_properties ( & engine, & [ prop] , & global_context, "dev" ) ;
773+
774+ assert_eq ! (
775+ ctx. get( "instance_class" ) . unwrap( ) ,
776+ "Cfg-Plain-Value-Not-Secret-4"
777+ ) ;
778+ let out = crate :: core:: secrets:: redact ( "class = 'Cfg-Plain-Value-Not-Secret-4'" ) ;
779+ assert ! ( out. contains( "Cfg-Plain-Value-Not-Secret-4" ) ) ;
780+ }
657781}
0 commit comments