From 10ecdc8d5ab2fd564807a0d40ce043193b321bb8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sat, 20 Sep 2025 13:31:05 -0700 Subject: [PATCH 1/8] Implement Display for secure types --- dsc_lib/src/configure/parameters.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index 62f95158c..501339593 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct Input { @@ -19,3 +19,12 @@ pub enum SecureKind { #[serde(rename = "secureObject")] SecureObject(Value), } + +impl Display for SecureKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SecureKind::SecureString(_) => write!(f, ""), + SecureKind::SecureObject(_) => write!(f, ""), + } + } +} From 78b789388691a80816cfed3836a86067e9b60c4a Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 21 Sep 2025 15:52:02 -0700 Subject: [PATCH 2/8] Fix consistent passing of `secureString` and `secureObject` --- dsc_lib/locales/en-us.toml | 1 + dsc_lib/src/configure/mod.rs | 15 +++++- dsc_lib/src/configure/parameters.rs | 50 ++++++++++++++++---- dsc_lib/src/dscresources/command_resource.rs | 4 +- dsc_lib/src/functions/parameters.rs | 10 ++-- dsc_lib/src/parser/expressions.rs | 49 +++++++++++++++++-- dscecho/src/echo.rs | 16 ++++++- dscecho/src/main.rs | 13 ++++- 8 files changed, 137 insertions(+), 21 deletions(-) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 9e301148c..c8fb0d70b 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -511,6 +511,7 @@ parsingIndexAccessor = "Parsing index accessor '%{index}'" indexNotFound = "Index value not found" invalidAccessorKind = "Invalid accessor kind: '%{kind}'" functionResult = "Function results: %{results}" +functionResultSecure = "Function result is secure" evalAccessors = "Evaluating accessors" memberNameNotFound = "Member '%{member}' not found" accessOnNonObject = "Member access on non-object value" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index d279e48cb..5fb90859e 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -3,6 +3,7 @@ use crate::configure::config_doc::{ExecutionKind, Metadata, Resource}; use crate::configure::context::{Context, ProcessMode}; +use crate::configure::parameters::is_secure_value; use crate::configure::{config_doc::RestartRequired, parameters::Input}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; @@ -214,7 +215,19 @@ fn add_metadata(dsc_resource: &DscResource, mut properties: Option { - Ok(serde_json::to_string(&properties)?) + let mut unsecure_properties = Map::new(); + for (key, value) in properties { + if is_secure_value(&value) { + if value.get("secureString").is_some() { + unsecure_properties.insert(key.clone(), "".into()); + } else if value.get("secureObject").is_some() { + unsecure_properties.insert(key.clone(), "".into()); + } + } else { + unsecure_properties.insert(key, value); + } + } + Ok(serde_json::to_string(&unsecure_properties)?) }, _ => { Ok(String::new()) diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index 501339593..ccc1168f2 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -12,19 +12,53 @@ pub struct Input { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields, untagged)] -pub enum SecureKind { +pub struct SecureString { #[serde(rename = "secureString")] - SecureString(String), + pub secure_string: String, +} + +impl Display for SecureString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +pub struct SecureObject { #[serde(rename = "secureObject")] - SecureObject(Value), + pub secure_object: Value, } -impl Display for SecureKind { +impl Display for SecureObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SecureKind::SecureString(_) => write!(f, ""), - SecureKind::SecureObject(_) => write!(f, ""), + write!(f, "") + } +} + +/// Check if a given JSON value is a secure value (either `SecureString` or `SecureObject`). +/// +/// # Arguments +/// +/// * `value` - The JSON value to check. +/// +/// # Returns +/// +/// `true` if the value is a secure value, `false` otherwise. +#[must_use] +pub fn is_secure_value(value: &Value) -> bool { + if let Some(obj) = value.as_object() { + if obj.len() == 1 && (obj.contains_key("secureString") || obj.contains_key("secureObject")) { + return true; } } + false +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, untagged)] +pub enum SecureKind { + #[serde(rename = "secureString")] + SecureString(SecureString), + #[serde(rename = "secureObject")] + SecureObject(SecureObject), } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index cdbd2afb3..49ffb01b3 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -717,9 +717,9 @@ async fn run_process_async(executable: &str, args: Option>, input: O #[allow(clippy::implicit_hasher)] #[tokio::main] pub async fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { - debug!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); if let Some(cwd) = cwd { - debug!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); + trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); } match run_process_async(executable, args, input, cwd, env, exit_codes).await { diff --git a/dsc_lib/src/functions/parameters.rs b/dsc_lib/src/functions/parameters.rs index 955477725..d211f5709 100644 --- a/dsc_lib/src/functions/parameters.rs +++ b/dsc_lib/src/functions/parameters.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::configure::config_doc::DataType; -use crate::configure::parameters::SecureKind; +use crate::configure::parameters::{SecureObject, SecureString}; use crate::DscError; use crate::configure::context::Context; use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; @@ -40,11 +40,15 @@ impl Function for Parameters { let Some(value) = value.as_str() else { return Err(DscError::Parser(t!("functions.parameters.keyNotString", key = key).to_string())); }; - let secure_string = SecureKind::SecureString(value.to_string()); + let secure_string = SecureString { + secure_string: value.to_string(), + }; Ok(serde_json::to_value(secure_string)?) }, DataType::SecureObject => { - let secure_object = SecureKind::SecureObject(value.clone()); + let secure_object = SecureObject { + secure_object: value.clone(), + }; Ok(serde_json::to_value(secure_object)?) }, _ => { diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index ceaebd9f1..795969316 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -7,6 +7,7 @@ use tracing::{debug, trace}; use tree_sitter::Node; use crate::configure::context::Context; +use crate::configure::parameters::is_secure_value; use crate::dscerror::DscError; use crate::functions::FunctionDispatcher; use crate::parser::functions::Function; @@ -113,10 +114,11 @@ impl Expression { /// This function will return an error if the expression fails to execute. pub fn invoke(&self, function_dispatcher: &FunctionDispatcher, context: &Context) -> Result { let result = self.function.invoke(function_dispatcher, context)?; - // skip trace if function is 'secret()' - if self.function.name() != "secret" { + if self.function.name() != "secret" && !is_secure_value(&result) { let result_json = serde_json::to_string(&result)?; trace!("{}", t!("parser.expression.functionResult", results = result_json)); + } else { + trace!("{}", t!("parser.expression.functionResultSecure")); } if self.accessors.is_empty() { Ok(result) @@ -124,6 +126,18 @@ impl Expression { else { debug!("{}", t!("parser.expression.evalAccessors")); let mut value = result; + let is_secure = is_secure_value(&value); + if is_secure { + // if a SecureString, extract the string value + if let Some(string) = value.get("secureString") { + if let Some(s) = string.as_str() { + value = Value::String(s.to_string()); + } + } else if let Some(obj) = value.get("secureObject") { + // if a SecureObject, extract the object value + value = obj.clone(); + } + } for accessor in &self.accessors { let mut index = Value::Null; match accessor { @@ -132,7 +146,12 @@ impl Expression { if !object.contains_key(member) { return Err(DscError::Parser(t!("parser.expression.memberNameNotFound", member = member).to_string())); } - value = object[member].clone(); + if is_secure { + // if the original value was a secure value, we need to convert the member value back to secure + value = convert_to_secure(&object[member]); + } else { + value = object[member].clone(); + } } else { return Err(DscError::Parser(t!("parser.expression.accessOnNonObject").to_string())); } @@ -169,3 +188,27 @@ impl Expression { } } } + +fn convert_to_secure(value: &Value) -> Value { + if let Some(string) = value.as_str() { + let secure_string = crate::configure::parameters::SecureString { + secure_string: string.to_string(), + }; + return serde_json::to_value(secure_string).unwrap_or(value.clone()); + } + + if let Some(obj) = value.as_object() { + if obj.len() == 1 && obj.contains_key("secureObject") { + let secure_object = crate::configure::parameters::SecureObject { + secure_object: obj["secureObject"].clone(), + }; + return serde_json::to_value(secure_object).unwrap_or(value.clone()); + } + } + + if let Some(array) = value.as_array() { + let new_array: Vec = array.iter().map(convert_to_secure).collect(); + return Value::Array(new_array); + } + value.clone() +} diff --git a/dscecho/src/echo.rs b/dscecho/src/echo.rs index 8485adb8b..39728af23 100644 --- a/dscecho/src/echo.rs +++ b/dscecho/src/echo.rs @@ -17,13 +17,25 @@ pub enum Output { #[serde(rename = "object")] Object(Value), #[serde(rename = "secureObject")] - SecureObject(Value), + SecureObject(SecureObject), #[serde(rename = "secureString")] - SecureString(String), + SecureString(SecureString), #[serde(rename = "string")] String(String), } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SecureString { + pub secure_string: String, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct SecureObject { + pub secure_object: Value, +} + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct Echo { diff --git a/dscecho/src/main.rs b/dscecho/src/main.rs index d7dd182f7..74d748561 100644 --- a/dscecho/src/main.rs +++ b/dscecho/src/main.rs @@ -8,7 +8,7 @@ use args::Args; use clap::Parser; use rust_i18n::{i18n, t}; use schemars::schema_for; -use crate::echo::Echo; +use crate::echo::{Echo, Output}; i18n!("locales", fallback = "en-us"); @@ -16,13 +16,22 @@ fn main() { let args = Args::parse(); match args.input { Some(input) => { - let echo = match serde_json::from_str::(&input) { + let mut echo = match serde_json::from_str::(&input) { Ok(echo) => echo, Err(err) => { eprintln!("{}: {err}", t!("main.invalidJson")); std::process::exit(1); } }; + match echo.output { + Output::SecureObject(_) => { + echo.output = Output::String("".to_string()); + }, + Output::SecureString(_) => { + echo.output = Output::String("".to_string()); + }, + _ => {} + } let json = serde_json::to_string(&echo).unwrap(); println!("{json}"); return; From 5c2f0e4b6bcb24e9331efe16cce7a0a051b1a2ef Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 21 Sep 2025 15:56:26 -0700 Subject: [PATCH 3/8] refactor some code --- dsc_lib/src/parser/expressions.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index 795969316..61f8d4d90 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -146,18 +146,13 @@ impl Expression { if !object.contains_key(member) { return Err(DscError::Parser(t!("parser.expression.memberNameNotFound", member = member).to_string())); } - if is_secure { - // if the original value was a secure value, we need to convert the member value back to secure - value = convert_to_secure(&object[member]); - } else { - value = object[member].clone(); - } + value = convert_to_secure(&object[member]); } else { return Err(DscError::Parser(t!("parser.expression.accessOnNonObject").to_string())); } }, Accessor::Index(index_value) => { - index = index_value.clone(); + index = convert_to_secure(index_value); }, Accessor::IndexExpression(expression) => { index = expression.invoke(function_dispatcher, context)?; @@ -174,7 +169,7 @@ impl Expression { if index >= array.len() { return Err(DscError::Parser(t!("parser.expression.indexOutOfBounds").to_string())); } - value = array[index].clone(); + value = convert_to_secure(&array[index]); } else { return Err(DscError::Parser(t!("parser.expression.indexOnNonArray").to_string())); } @@ -190,6 +185,10 @@ impl Expression { } fn convert_to_secure(value: &Value) -> Value { + if !is_secure_value(value) { + return value.clone(); + } + if let Some(string) = value.as_str() { let secure_string = crate::configure::parameters::SecureString { secure_string: string.to_string(), From 6da122af55d786a8d3b82dd28db1124bff623a5c Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 21 Sep 2025 22:58:43 -0700 Subject: [PATCH 4/8] fix Echo to respect secure types --- dsc/examples/secure_parameters.dsc.yaml | 5 +++ .../secure_parameters_shown.parameters.yaml | 5 +++ dsc_lib/src/configure/mod.rs | 15 +------ dsc_lib/src/configure/parameters.rs | 4 +- dsc_lib/src/dscresources/command_resource.rs | 7 ++- dsc_lib/src/dscresources/dscresource.rs | 44 +++++++++++++++++-- dsc_lib/src/parser/expressions.rs | 22 +++++++--- dscecho/src/echo.rs | 9 +++- dscecho/src/main.rs | 15 +++---- 9 files changed, 88 insertions(+), 38 deletions(-) create mode 100644 dsc/examples/secure_parameters_shown.parameters.yaml diff --git a/dsc/examples/secure_parameters.dsc.yaml b/dsc/examples/secure_parameters.dsc.yaml index 434625559..f57c6eb93 100644 --- a/dsc/examples/secure_parameters.dsc.yaml +++ b/dsc/examples/secure_parameters.dsc.yaml @@ -4,12 +4,17 @@ parameters: type: secureString myObject: type: secureObject + showSecrets: + type: bool + defaultValue: false resources: - name: Echo 1 type: Microsoft.DSC.Debug/Echo properties: output: "[parameters('myString')]" + showSecrets: "[parameters('showSecrets')]" - name: Echo 2 type: Microsoft.DSC.Debug/Echo properties: output: "[parameters('myObject').myProperty]" + showSecrets: "[parameters('showSecrets')]" diff --git a/dsc/examples/secure_parameters_shown.parameters.yaml b/dsc/examples/secure_parameters_shown.parameters.yaml new file mode 100644 index 000000000..1e8bc0364 --- /dev/null +++ b/dsc/examples/secure_parameters_shown.parameters.yaml @@ -0,0 +1,5 @@ +parameters: + myString: mySecret + myObject: + myProperty: mySecretProperty + showSecrets: true diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 5fb90859e..d279e48cb 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -3,7 +3,6 @@ use crate::configure::config_doc::{ExecutionKind, Metadata, Resource}; use crate::configure::context::{Context, ProcessMode}; -use crate::configure::parameters::is_secure_value; use crate::configure::{config_doc::RestartRequired, parameters::Input}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; @@ -215,19 +214,7 @@ fn add_metadata(dsc_resource: &DscResource, mut properties: Option { - let mut unsecure_properties = Map::new(); - for (key, value) in properties { - if is_secure_value(&value) { - if value.get("secureString").is_some() { - unsecure_properties.insert(key.clone(), "".into()); - } else if value.get("secureObject").is_some() { - unsecure_properties.insert(key.clone(), "".into()); - } - } else { - unsecure_properties.insert(key, value); - } - } - Ok(serde_json::to_string(&unsecure_properties)?) + Ok(serde_json::to_string(&properties)?) }, _ => { Ok(String::new()) diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index ccc1168f2..90beaac89 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -19,7 +19,7 @@ pub struct SecureString { impl Display for SecureString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "") + write!(f, "") } } @@ -31,7 +31,7 @@ pub struct SecureObject { impl Display for SecureObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "") + write!(f, "") } } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 49ffb01b3..b30ce3543 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -9,7 +9,7 @@ use serde_json::{Map, Value}; use std::{collections::HashMap, env, process::Stdio}; use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}; use crate::dscerror::DscError; -use super::{dscresource::get_diff, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; +use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}}; use tracing::{error, warn, info, debug, trace}; use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; @@ -286,7 +286,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Ok(TestResult::Group(group_test_response)); } - let expected_value: Value = serde_json::from_str(expected)?; + let mut expected_value: Value = serde_json::from_str(expected)?; match test.returns { Some(ReturnKind::State) => { let actual_value: Value = match serde_json::from_str(&stdout){ @@ -297,6 +297,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re }; let in_desired_state = get_desired_state(&actual_value)?; let diff_properties = get_diff(&expected_value, &actual_value); + expected_value = redact(&expected_value); Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state: actual_value, @@ -315,6 +316,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_properties)?; + expected_value = redact(&expected_value); let in_desired_state = get_desired_state(&actual_value)?; Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, @@ -339,6 +341,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re } }; let diff_properties = get_diff( &expected_value, &actual_state); + expected_value = redact(&expected_value); Ok(TestResult::Resource(ResourceTestResponse { desired_state: expected_value, actual_state, diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 3dfb9ced5..31ba46e69 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{config_doc::{Configuration, ExecutionKind, Resource}, Configurator, context::ProcessMode}, dscresources::resource_manifest::Kind}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::is_secure_value}, dscresources::resource_manifest::Kind}; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use dscerror::DscError; use jsonschema::Validator; @@ -311,7 +311,7 @@ impl Invoke for DscResource { let TestResult::Resource(ref resource_result) = result.results[0].result else { return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "test", resource = self.type_name).to_string())); }; - let desired_state = resource_result.desired_state + let mut desired_state = resource_result.desired_state .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "desiredState", property_type = "object").to_string()))? .get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))? .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "resources", property_type = "array").to_string()))?[0] @@ -324,6 +324,7 @@ impl Invoke for DscResource { .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); let diff_properties = get_diff(&desired_state, &actual_state); + desired_state = redact(&desired_state); let test_result = TestResult::Resource(ResourceTestResponse { desired_state, actual_state, @@ -346,7 +347,7 @@ impl Invoke for DscResource { let resource_manifest = import_manifest(manifest.clone())?; if resource_manifest.test.is_none() { let get_result = self.get(expected)?; - let desired_state = serde_json::from_str(expected)?; + let mut desired_state = serde_json::from_str(expected)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -360,6 +361,7 @@ impl Invoke for DscResource { } }; let diff_properties = get_diff( &desired_state, &actual_state); + desired_state = redact(&desired_state); let test_result = TestResult::Resource(ResourceTestResponse { desired_state, actual_state, @@ -491,6 +493,37 @@ pub fn get_well_known_properties() -> HashMap { ]) } +/// Checks if the JSON value is sensitive and should be redacted +/// +/// # Arguments +/// +/// * `value` - The JSON value to check +/// +/// # Returns +/// +/// Original value if not sensitive, otherwise a redacted value +pub fn redact(value: &Value) -> Value { + trace!("Redacting value: {value}"); + if is_secure_value(value) { + return Value::String("".to_string()); + } + + if let Some(map) = value.as_object() { + let mut new_map = Map::new(); + for (key, val) in map { + new_map.insert(key.clone(), redact(val)); + } + return Value::Object(new_map); + } + + if let Some(array) = value.as_array() { + let new_array: Vec = array.iter().map(|val| redact(val)).collect(); + return Value::Array(new_array); + } + + value.clone() +} + #[must_use] /// Performs a comparison of two JSON Values if the expected is a strict subset of the actual /// @@ -524,6 +557,11 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { } for (key, value) in &*map { + if is_secure_value(&value) { + // skip secure values as they are not comparable + continue; + } + if value.is_object() { let sub_diff = get_diff(value, &actual[key]); if !sub_diff.is_empty() { diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index 61f8d4d90..78b370765 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -146,13 +146,21 @@ impl Expression { if !object.contains_key(member) { return Err(DscError::Parser(t!("parser.expression.memberNameNotFound", member = member).to_string())); } - value = convert_to_secure(&object[member]); + if is_secure { + value = convert_to_secure(&object[member]); + } else { + value = object[member].clone(); + } } else { return Err(DscError::Parser(t!("parser.expression.accessOnNonObject").to_string())); } }, Accessor::Index(index_value) => { - index = convert_to_secure(index_value); + if is_secure { + index = convert_to_secure(index_value); + } else { + index = index_value.clone(); + } }, Accessor::IndexExpression(expression) => { index = expression.invoke(function_dispatcher, context)?; @@ -169,7 +177,11 @@ impl Expression { if index >= array.len() { return Err(DscError::Parser(t!("parser.expression.indexOutOfBounds").to_string())); } - value = convert_to_secure(&array[index]); + if is_secure { + value = convert_to_secure(&array[index]); + } else { + value = array[index].clone(); + } } else { return Err(DscError::Parser(t!("parser.expression.indexOnNonArray").to_string())); } @@ -185,10 +197,6 @@ impl Expression { } fn convert_to_secure(value: &Value) -> Value { - if !is_secure_value(value) { - return value.clone(); - } - if let Some(string) = value.as_str() { let secure_string = crate::configure::parameters::SecureString { secure_string: string.to_string(), diff --git a/dscecho/src/echo.rs b/dscecho/src/echo.rs index 39728af23..f91eca84f 100644 --- a/dscecho/src/echo.rs +++ b/dscecho/src/echo.rs @@ -14,25 +14,28 @@ pub enum Output { Bool(bool), #[serde(rename = "number")] Number(i64), - #[serde(rename = "object")] - Object(Value), #[serde(rename = "secureObject")] SecureObject(SecureObject), #[serde(rename = "secureString")] SecureString(SecureString), #[serde(rename = "string")] String(String), + // Object has to be last so it doesn't get matched first + #[serde(rename = "object")] + Object(Value), } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SecureString { + #[serde(rename = "secureString")] pub secure_string: String, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct SecureObject { + #[serde(rename = "secureObject")] pub secure_object: Value, } @@ -40,4 +43,6 @@ pub struct SecureObject { #[serde(deny_unknown_fields)] pub struct Echo { pub output: Output, + #[serde(rename = "showSecrets", skip_serializing_if = "Option::is_none")] + pub show_secrets: Option, } diff --git a/dscecho/src/main.rs b/dscecho/src/main.rs index 74d748561..a558088fd 100644 --- a/dscecho/src/main.rs +++ b/dscecho/src/main.rs @@ -23,14 +23,13 @@ fn main() { std::process::exit(1); } }; - match echo.output { - Output::SecureObject(_) => { - echo.output = Output::String("".to_string()); - }, - Output::SecureString(_) => { - echo.output = Output::String("".to_string()); - }, - _ => {} + if echo.show_secrets != Some(true) { + match &echo.output { + Output::SecureString(_) | Output::SecureObject(_) => { + echo.output = Output::String("".to_string()); + }, + _ => {} + } } let json = serde_json::to_string(&echo).unwrap(); println!("{json}"); From 44836f2f42ca5c644e8af780171bde9f0e4ddc73 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Sun, 21 Sep 2025 23:00:24 -0700 Subject: [PATCH 5/8] fix clippy --- dsc_lib/src/dscresources/dscresource.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 31ba46e69..f6efd8038 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -517,7 +517,7 @@ pub fn redact(value: &Value) -> Value { } if let Some(array) = value.as_array() { - let new_array: Vec = array.iter().map(|val| redact(val)).collect(); + let new_array: Vec = array.iter().map(redact).collect(); return Value::Array(new_array); } @@ -557,7 +557,7 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { } for (key, value) in &*map { - if is_secure_value(&value) { + if is_secure_value(value) { // skip secure values as they are not comparable continue; } From be4abd4bf9877ede057beafcf44b24b9c8247b0b Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 22 Sep 2025 07:04:40 -0700 Subject: [PATCH 6/8] fix tests --- dsc/tests/dsc_extension_secret.tests.ps1 | 3 ++- dsc/tests/dsc_parameters.tests.ps1 | 13 ++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/dsc/tests/dsc_extension_secret.tests.ps1 b/dsc/tests/dsc_extension_secret.tests.ps1 index 1535dc213..5d70610e4 100644 --- a/dsc/tests/dsc_extension_secret.tests.ps1 +++ b/dsc/tests/dsc_extension_secret.tests.ps1 @@ -144,11 +144,12 @@ Describe 'Tests for the secret() function and extensions' { type: Microsoft.DSC.Debug/Echo properties: output: "[parameters('myString')]" + showSecrets: true '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.results.Count | Should -Be 1 - $out.results[0].result.actualState.Output | Should -BeExactly 'Hello' + $out.results[0].result.actualState.Output.secureString | Should -BeExactly 'Hello' } It 'Allows to pass in secret() through variables' { diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index 2a2e569f0..79e924f11 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -274,11 +274,18 @@ Describe 'Parameters tests' { $out.results[0].result.inDesiredState | Should -BeTrue } - It 'secure types can be passed as objects to resources' { + It 'secure types can be passed as objects to resources but redacted in output' { $out = dsc config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results[0].result.actualState.output | Should -BeExactly 'mySecret' - $out.results[1].result.actualState.output | Should -BeExactly 'mySecretProperty' + $out.results[0].result.actualState.output | Should -BeExactly '' + $out.results[1].result.actualState.output | Should -BeExactly '' + } + + It 'secure types can be passed as objects to resources' { + $out = dsc config -f $PSScriptRoot/../examples/secure_parameters_shown.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output.secureString | Should -BeExactly 'mySecret' + $out.results[1].result.actualState.output.secureString | Should -BeExactly 'mySecretProperty' } It 'parameter types are validated for ' -TestCases @( From 91c6c1cecd014307feceda4542efe2d93e58feb8 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 22 Sep 2025 12:04:36 -0700 Subject: [PATCH 7/8] fix handling of arrays --- dsc/examples/secure_parameters.dsc.yaml | 14 ++++- .../secure_parameters.parameters.yaml | 3 + .../secure_parameters_shown.parameters.yaml | 3 + dsc/tests/dsc_parameters.tests.ps1 | 10 +++- dsc_lib/src/configure/parameters.rs | 6 +- dsc_lib/src/dscresources/dscresource.rs | 5 +- dsc_lib/src/parser/expressions.rs | 19 +++++-- dscecho/locales/en-us.toml | 1 - dscecho/src/main.rs | 55 +++++++++++++++++-- 9 files changed, 95 insertions(+), 21 deletions(-) diff --git a/dsc/examples/secure_parameters.dsc.yaml b/dsc/examples/secure_parameters.dsc.yaml index f57c6eb93..82c45d7e5 100644 --- a/dsc/examples/secure_parameters.dsc.yaml +++ b/dsc/examples/secure_parameters.dsc.yaml @@ -8,13 +8,23 @@ parameters: type: bool defaultValue: false resources: - - name: Echo 1 + - name: SecureString type: Microsoft.DSC.Debug/Echo properties: output: "[parameters('myString')]" showSecrets: "[parameters('showSecrets')]" - - name: Echo 2 + - name: SecureObject type: Microsoft.DSC.Debug/Echo properties: output: "[parameters('myObject').myProperty]" showSecrets: "[parameters('showSecrets')]" + - name: SecureArray + type: Microsoft.DSC.Debug/Echo + properties: + output: "[parameters('myObject').myArray]" + showSecrets: "[parameters('showSecrets')]" + - name: SecureArrayIndexed + type: Microsoft.DSC.Debug/Echo + properties: + output: "[parameters('myObject').myArray[1]]" + showSecrets: "[parameters('showSecrets')]" diff --git a/dsc/examples/secure_parameters.parameters.yaml b/dsc/examples/secure_parameters.parameters.yaml index 2352b4e92..c90ae080e 100644 --- a/dsc/examples/secure_parameters.parameters.yaml +++ b/dsc/examples/secure_parameters.parameters.yaml @@ -2,3 +2,6 @@ parameters: myString: mySecret myObject: myProperty: mySecretProperty + myArray: + - item1 + - item2 diff --git a/dsc/examples/secure_parameters_shown.parameters.yaml b/dsc/examples/secure_parameters_shown.parameters.yaml index 1e8bc0364..c38bb4416 100644 --- a/dsc/examples/secure_parameters_shown.parameters.yaml +++ b/dsc/examples/secure_parameters_shown.parameters.yaml @@ -2,4 +2,7 @@ parameters: myString: mySecret myObject: myProperty: mySecretProperty + myArray: + - item1 + - item2 showSecrets: true diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index 79e924f11..f87bdc5b7 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -275,17 +275,25 @@ Describe 'Parameters tests' { } It 'secure types can be passed as objects to resources but redacted in output' { - $out = dsc config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json + $out = dsc -l trace config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 + $out.results.Count | Should -Be 4 $out.results[0].result.actualState.output | Should -BeExactly '' $out.results[1].result.actualState.output | Should -BeExactly '' + $out.results[2].result.actualState.output[0] | Should -BeExactly '' + $out.results[2].result.actualState.output[1] | Should -BeExactly '' + $out.results[3].result.actualState.output | Should -BeExactly '' } It 'secure types can be passed as objects to resources' { $out = dsc config -f $PSScriptRoot/../examples/secure_parameters_shown.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 + $out.results.Count | Should -Be 4 $out.results[0].result.actualState.output.secureString | Should -BeExactly 'mySecret' $out.results[1].result.actualState.output.secureString | Should -BeExactly 'mySecretProperty' + $out.results[2].result.actualState.output[0].secureString | Should -BeExactly 'item1' + $out.results[2].result.actualState.output[1].secureString | Should -BeExactly 'item2' + $out.results[3].result.actualState.output.secureObject.secureString | Should -BeExactly 'item2' } It 'parameter types are validated for ' -TestCases @( diff --git a/dsc_lib/src/configure/parameters.rs b/dsc_lib/src/configure/parameters.rs index 90beaac89..549730b2f 100644 --- a/dsc_lib/src/configure/parameters.rs +++ b/dsc_lib/src/configure/parameters.rs @@ -11,6 +11,8 @@ pub struct Input { pub parameters: HashMap, } +pub const SECURE_VALUE_REDACTED: &str = ""; + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct SecureString { #[serde(rename = "secureString")] @@ -19,7 +21,7 @@ pub struct SecureString { impl Display for SecureString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "") + write!(f, "{SECURE_VALUE_REDACTED}") } } @@ -31,7 +33,7 @@ pub struct SecureObject { impl Display for SecureObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "") + write!(f, "{SECURE_VALUE_REDACTED}") } } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index f6efd8038..0ee2e0796 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::is_secure_value}, dscresources::resource_manifest::Kind}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::Kind}; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use dscerror::DscError; use jsonschema::Validator; @@ -503,9 +503,8 @@ pub fn get_well_known_properties() -> HashMap { /// /// Original value if not sensitive, otherwise a redacted value pub fn redact(value: &Value) -> Value { - trace!("Redacting value: {value}"); if is_secure_value(value) { - return Value::String("".to_string()); + return Value::String(SECURE_VALUE_REDACTED.to_string()); } if let Some(map) = value.as_object() { diff --git a/dsc_lib/src/parser/expressions.rs b/dsc_lib/src/parser/expressions.rs index 78b370765..16879126a 100644 --- a/dsc_lib/src/parser/expressions.rs +++ b/dsc_lib/src/parser/expressions.rs @@ -196,6 +196,15 @@ impl Expression { } } +/// Convert a JSON value to a secure value if it is a string or an array of strings. +/// +/// Arguments +/// +/// * `value` - The JSON value to convert. +/// +/// Returns +/// +/// The converted JSON value. fn convert_to_secure(value: &Value) -> Value { if let Some(string) = value.as_str() { let secure_string = crate::configure::parameters::SecureString { @@ -205,12 +214,10 @@ fn convert_to_secure(value: &Value) -> Value { } if let Some(obj) = value.as_object() { - if obj.len() == 1 && obj.contains_key("secureObject") { - let secure_object = crate::configure::parameters::SecureObject { - secure_object: obj["secureObject"].clone(), - }; - return serde_json::to_value(secure_object).unwrap_or(value.clone()); - } + let secure_object = crate::configure::parameters::SecureObject { + secure_object: serde_json::Value::Object(obj.clone()), + }; + return serde_json::to_value(secure_object).unwrap_or(value.clone()); } if let Some(array) = value.as_array() { diff --git a/dscecho/locales/en-us.toml b/dscecho/locales/en-us.toml index 2dfeaedbd..8b121bbd1 100644 --- a/dscecho/locales/en-us.toml +++ b/dscecho/locales/en-us.toml @@ -2,4 +2,3 @@ _version = 1 [main] invalidJson = "Error JSON does not match schema" -noInput = "No input provided." diff --git a/dscecho/src/main.rs b/dscecho/src/main.rs index a558088fd..c90573bb8 100644 --- a/dscecho/src/main.rs +++ b/dscecho/src/main.rs @@ -8,10 +8,13 @@ use args::Args; use clap::Parser; use rust_i18n::{i18n, t}; use schemars::schema_for; +use serde_json::{Map, Value}; use crate::echo::{Echo, Output}; i18n!("locales", fallback = "en-us"); +const SECURE_VALUE_REDACTED: &str = ""; + fn main() { let args = Args::parse(); match args.input { @@ -24,9 +27,21 @@ fn main() { } }; if echo.show_secrets != Some(true) { - match &echo.output { + match echo.output { Output::SecureString(_) | Output::SecureObject(_) => { - echo.output = Output::String("".to_string()); + echo.output = Output::String(SECURE_VALUE_REDACTED.to_string()); + }, + Output::Array(ref mut arr) => { + for item in arr.iter_mut() { + if is_secure_value(item) { + *item = Value::String(SECURE_VALUE_REDACTED.to_string()); + } else { + *item = redact(item); + } + } + }, + Output::Object(ref mut obj) => { + *obj = redact(obj); }, _ => {} } @@ -36,11 +51,39 @@ fn main() { return; }, None => { - eprintln!("{}", t!("main.noInput")); + let schema = schema_for!(Echo); + let json = serde_json::to_string_pretty(&schema).unwrap(); + println!("{json}"); + } + } +} + +fn is_secure_value(value: &Value) -> bool { + if let Some(obj) = value.as_object() { + if obj.len() == 1 && (obj.contains_key("secureString") || obj.contains_key("secureObject")) { + return true; } } + false +} + +pub fn redact(value: &Value) -> Value { + if is_secure_value(value) { + return Value::String(SECURE_VALUE_REDACTED.to_string()); + } + + if let Some(map) = value.as_object() { + let mut new_map = Map::new(); + for (key, val) in map { + new_map.insert(key.clone(), redact(val)); + } + return Value::Object(new_map); + } + + if let Some(array) = value.as_array() { + let new_array: Vec = array.iter().map(redact).collect(); + return Value::Array(new_array); + } - let schema = schema_for!(Echo); - let json = serde_json::to_string_pretty(&schema).unwrap(); - println!("{json}"); + value.clone() } From aa67d4500857ccdb23edc4897e2492f6c5c3bf1d Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Mon, 22 Sep 2025 13:16:55 -0700 Subject: [PATCH 8/8] fix tests and clippy --- dsc/tests/dsc_parameters.tests.ps1 | 67 +++++++++++++++----- dsc_lib/src/dscresources/command_resource.rs | 7 +- dscecho/src/main.rs | 67 ++++++++++---------- 3 files changed, 88 insertions(+), 53 deletions(-) diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index f87bdc5b7..2b57672fd 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -274,26 +274,61 @@ Describe 'Parameters tests' { $out.results[0].result.inDesiredState | Should -BeTrue } - It 'secure types can be passed as objects to resources but redacted in output' { - $out = dsc -l trace config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml 2> $TestDrive/error.log | ConvertFrom-Json + It 'secure types can be passed as objects to resources but redacted in output: ' -TestCases @( + @{ operation = 'get'; property = 'actualState' } + @{ operation = 'set'; property = 'beforeState' } + @{ operation = 'set'; property = 'afterState' } + @{ operation = 'test'; property = 'desiredState' } + @{ operation = 'test'; property = 'actualState' } + @{ operation = 'export'; property = $null } + ) { + param($operation, $property) + + $out = dsc -l trace config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml $operation -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results.Count | Should -Be 4 - $out.results[0].result.actualState.output | Should -BeExactly '' - $out.results[1].result.actualState.output | Should -BeExactly '' - $out.results[2].result.actualState.output[0] | Should -BeExactly '' - $out.results[2].result.actualState.output[1] | Should -BeExactly '' - $out.results[3].result.actualState.output | Should -BeExactly '' + if ($operation -eq 'export') { + $out.resources.Count | Should -Be 4 + $out.resources[0].properties.output | Should -BeExactly '' + $out.resources[1].properties.output | Should -BeExactly '' + $out.resources[2].properties.output[0] | Should -BeExactly '' + $out.resources[2].properties.output[1] | Should -BeExactly '' + $out.resources[3].properties.output | Should -BeExactly '' + } else { + $out.results.Count | Should -Be 4 -Because ($out | ConvertTo-Json -Dep 10 | Out-String) + $out.results[0].result.$property.output | Should -BeExactly '' -Because ($out | ConvertTo-Json -Dep 10 | Out-String) + $out.results[1].result.$property.output | Should -BeExactly '' + $out.results[2].result.$property.output[0] | Should -BeExactly '' + $out.results[2].result.$property.output[1] | Should -BeExactly '' + $out.results[3].result.$property.output | Should -BeExactly '' + } } - It 'secure types can be passed as objects to resources' { - $out = dsc config -f $PSScriptRoot/../examples/secure_parameters_shown.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json + It 'secure types can be passed as objects to resources: ' -TestCases @( + # `set` beforeState is redacted in output, `test` desiredState is redacted in output so those test cases are not included here + @{ operation = 'get'; property = 'actualState' } + @{ operation = 'set'; property = 'afterState' } + @{ operation = 'test'; property = 'actualState' } + @{ operation = 'export'; property = $null } + ) { + param($operation, $property) + + $out = dsc config -f $PSScriptRoot/../examples/secure_parameters_shown.parameters.yaml $operation -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results.Count | Should -Be 4 - $out.results[0].result.actualState.output.secureString | Should -BeExactly 'mySecret' - $out.results[1].result.actualState.output.secureString | Should -BeExactly 'mySecretProperty' - $out.results[2].result.actualState.output[0].secureString | Should -BeExactly 'item1' - $out.results[2].result.actualState.output[1].secureString | Should -BeExactly 'item2' - $out.results[3].result.actualState.output.secureObject.secureString | Should -BeExactly 'item2' + if ($operation -eq 'export') { + $out.resources.Count | Should -Be 4 -Because ($out | ConvertTo-Json -Dep 10 | Out-String) + $out.resources[0].properties.output.secureString | Should -BeExactly 'mySecret' + $out.resources[1].properties.output.secureString | Should -BeExactly 'mySecretProperty' + $out.resources[2].properties.output[0].secureString | Should -BeExactly 'item1' + $out.resources[2].properties.output[1].secureString | Should -BeExactly 'item2' + $out.resources[3].properties.output.secureObject.secureString | Should -BeExactly 'item2' + } else { + $out.results.Count | Should -Be 4 + $out.results[0].result.$property.output.secureString | Should -BeExactly 'mySecret' -Because ($out | ConvertTo-Json -Dep 10 | Out-String) + $out.results[1].result.$property.output.secureString | Should -BeExactly 'mySecretProperty' + $out.results[2].result.$property.output[0].secureString | Should -BeExactly 'item1' + $out.results[2].result.$property.output[1].secureString | Should -BeExactly 'item2' + $out.results[3].result.$property.output.secureObject.secureString | Should -BeExactly 'item2' + } } It 'parameter types are validated for ' -TestCases @( diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index b30ce3543..dc89e2b34 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -120,8 +120,9 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te }; if in_desired_state && execution_type == &ExecutionKind::Actual { + let before_state = redact(&serde_json::from_str(desired)?); return Ok(SetResult::Resource(ResourceSetResponse{ - before_state: serde_json::from_str(desired)?, + before_state, after_state: actual_state, changed_properties: None, })); @@ -152,7 +153,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te else { return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr)); }; - let pre_state = if pre_state_value.is_object() { + let mut pre_state = if pre_state_value.is_object() { let mut pre_state_map: Map = serde_json::from_value(pre_state_value)?; // if the resource is an adapter, then the `get` will return a `result`, but a full `set` expects the before state to be `resources` @@ -200,6 +201,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te // for changed_properties, we compare post state to pre state let diff_properties = get_diff( &actual_value, &pre_state); + pre_state = redact(&pre_state); Ok(SetResult::Resource(ResourceSetResponse{ before_state: pre_state, after_state: actual_value, @@ -241,6 +243,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te } }; let diff_properties = get_diff( &actual_state, &pre_state); + pre_state = redact(&pre_state); Ok(SetResult::Resource(ResourceSetResponse { before_state: pre_state, after_state: actual_state, diff --git a/dscecho/src/main.rs b/dscecho/src/main.rs index c90573bb8..6845211dc 100644 --- a/dscecho/src/main.rs +++ b/dscecho/src/main.rs @@ -17,45 +17,42 @@ const SECURE_VALUE_REDACTED: &str = ""; fn main() { let args = Args::parse(); - match args.input { - Some(input) => { - let mut echo = match serde_json::from_str::(&input) { - Ok(echo) => echo, - Err(err) => { - eprintln!("{}: {err}", t!("main.invalidJson")); - std::process::exit(1); - } - }; - if echo.show_secrets != Some(true) { - match echo.output { - Output::SecureString(_) | Output::SecureObject(_) => { - echo.output = Output::String(SECURE_VALUE_REDACTED.to_string()); - }, - Output::Array(ref mut arr) => { - for item in arr.iter_mut() { - if is_secure_value(item) { - *item = Value::String(SECURE_VALUE_REDACTED.to_string()); - } else { - *item = redact(item); - } + if let Some(input) = args.input { + let mut echo = match serde_json::from_str::(&input) { + Ok(echo) => echo, + Err(err) => { + eprintln!("{}: {err}", t!("main.invalidJson")); + std::process::exit(1); + } + }; + if echo.show_secrets != Some(true) { + match echo.output { + Output::SecureString(_) | Output::SecureObject(_) => { + echo.output = Output::String(SECURE_VALUE_REDACTED.to_string()); + }, + Output::Array(ref mut arr) => { + for item in arr.iter_mut() { + if is_secure_value(item) { + *item = Value::String(SECURE_VALUE_REDACTED.to_string()); + } else { + *item = redact(item); } - }, - Output::Object(ref mut obj) => { - *obj = redact(obj); - }, - _ => {} - } + } + }, + Output::Object(ref mut obj) => { + *obj = redact(obj); + }, + _ => {} } - let json = serde_json::to_string(&echo).unwrap(); - println!("{json}"); - return; - }, - None => { - let schema = schema_for!(Echo); - let json = serde_json::to_string_pretty(&schema).unwrap(); - println!("{json}"); } + let json = serde_json::to_string(&echo).unwrap(); + println!("{json}"); + return; } + + let schema = schema_for!(Echo); + let json = serde_json::to_string_pretty(&schema).unwrap(); + println!("{json}"); } fn is_secure_value(value: &Value) -> bool {