diff --git a/Cargo.lock b/Cargo.lock index da9d04550..96aeab039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4239,15 +4239,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_feature" -version = "0.1.0" -dependencies = [ - "rust-i18n", - "serde", - "serde_json", -] - [[package]] name = "windows_firewall" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b792f8307..616eb8f65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ default-members = [ "lib/dsc-lib-registry", "resources/runcommandonset", "lib/dsc-lib-security_context", + "resources/dism_dsc", "resources/sshdconfig", "resources/WindowsUpdate", "resources/windows_service", @@ -57,8 +58,7 @@ default-members = [ "grammars/tree-sitter-dscexpression", "grammars/tree-sitter-ssh-server-config", "y2j", - "xtask", - "resources/dism_dsc" + "xtask" ] [workspace.metadata.groups] diff --git a/dsc/tests/dsc_extension_secret.tests.ps1 b/dsc/tests/dsc_extension_secret.tests.ps1 index 9496aa62c..92008d497 100644 --- a/dsc/tests/dsc_extension_secret.tests.ps1 +++ b/dsc/tests/dsc_extension_secret.tests.ps1 @@ -19,6 +19,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('MySecret')]" '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -34,6 +35,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('DifferentSecret', 'VaultA')]" '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -49,6 +51,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('NonExistentSecret')]" '@ dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -64,6 +67,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('MySecret', 'NonExistentVault')]" '@ dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -79,6 +83,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('DuplicateSecret')]" '@ dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -94,6 +99,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('DuplicateSecret', 'Vault1')]" '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -109,6 +115,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('DuplicateSame')]" '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -124,6 +131,7 @@ Describe 'Tests for the secret() function and extensions' { - name: Echo type: Microsoft.DSC.Debug/Echo properties: + showSecrets: true output: "[secret('MultiLine')]" '@ dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json @@ -147,9 +155,9 @@ Describe 'Tests for the secret() function and extensions' { showSecrets: true '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log) $out.results.Count | Should -Be 1 - $out.results[0].result.actualState.Output.secureString | Should -BeExactly 'Hello' + $out.results[0].result.actualState.Output | Should -BeExactly 'Hello' -Because (Get-Content -Raw -Path $TestDrive/error.log) } It 'Allows to pass in secret() through variables' { @@ -162,6 +170,7 @@ Describe 'Tests for the secret() function and extensions' { type: Microsoft.DSC.Debug/Echo properties: output: "[variables('myString')]" + showSecrets: true '@ $out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index b1b7a0b81..09f5c4f65 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -312,9 +312,9 @@ resources: configurationFile: "[concat('$echoConfigPathParent', '$directorySeparator', '$echoConfigPathLeaf')]" "@ - $out = dsc config set -i $includeConfig | ConvertFrom-Json + $out = dsc -l trace config set -i $includeConfig 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.results[0].result.beforeState[0].name | Should -Be 'one' + $out.results[0].result.beforeState[0].name | Should -Be 'one' -Because (Get-Content -Path $TestDrive/error.log -Raw) $out.results[0].result.beforeState[0].type | Should -Be 'Microsoft.DSC.Debug/Echo' $out.results[0].result.afterState[0].result.afterState.output | Should -Be 'Hello World' $out.hadErrors | Should -Be $false diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index e5ec560c1..a880301b0 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -316,18 +316,18 @@ Describe 'Parameters tests' { $LASTEXITCODE | Should -Be 0 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' + $out.resources[0].properties.output | Should -BeExactly 'mySecret' + $out.resources[1].properties.output | Should -BeExactly 'mySecretProperty' + $out.resources[2].properties.output[0] | Should -BeExactly 'item1' + $out.resources[2].properties.output[1] | Should -BeExactly 'item2' + $out.resources[3].properties.output | 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' + $out.results[0].result.$property.output | Should -BeExactly 'mySecret' -Because ($out | ConvertTo-Json -Dep 10 | Out-String) + $out.results[1].result.$property.output | Should -BeExactly 'mySecretProperty' + $out.results[2].result.$property.output[0] | Should -BeExactly 'item1' + $out.results[2].result.$property.output[1] | Should -BeExactly 'item2' + $out.results[3].result.$property.output | Should -BeExactly 'item2' } } diff --git a/extensions/test/secret/secret.ps1 b/extensions/test/secret/secret.ps1 index 33fac8530..79650c1c1 100644 --- a/extensions/test/secret/secret.ps1 +++ b/extensions/test/secret/secret.ps1 @@ -11,6 +11,8 @@ param( [string]$Vault ) +$ErrorActionPreference = 'Ignore' + $secretsOne = @{ Vault1 = @{ MySecret = 'Hello' diff --git a/lib/dsc-lib-jsonschema/.versions.json b/lib/dsc-lib-jsonschema/.versions.json index ccb9af183..15f6ca427 100644 --- a/lib/dsc-lib-jsonschema/.versions.json +++ b/lib/dsc-lib-jsonschema/.versions.json @@ -1,10 +1,11 @@ { "latestMajor": "V3", "latestMinor": "V3_2", - "latestPatch": "V3_2_1", + "latestPatch": "V3_2_2", "all": [ "V3", "V3_2", + "V3_2_2", "V3_2_1", "V3_2_0", "V3_1", diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 83316a729..d501fdbac 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -58,6 +58,8 @@ setParameter = "Set parameter '%{name}' to '%{value}'" parameterNotDefined = "Parameter '%{name}' is not defined in configuration" noVariables = "No variables defined in configuration" setVariable = "Set variable '%{name}' to '%{value}'" +parameterNotSecureString = "Parameter '%{name}' is not a secure string" +parameterNotSecureObject = "Parameter '%{name}' is not a secure object" parameterNotString = "Parameter '%{name}' is not a string" parameterNotInteger = "Parameter '%{name}' is not an integer" parameterNotBoolean = "Parameter '%{name}' is not a boolean" @@ -95,6 +97,8 @@ metadataRefreshEnvOnlyAffectsSet = "Resource returned '_refreshEnv' which indica metadataRefreshEnvNoOperationContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no operation context is available" metadataRefreshEnvNoContext = "Resource returned '_refreshEnv' which indicates environment variable refresh is needed but no context is available" copyDeprecated = "Copy for loop '%{name}' is deprecated and will be removed in a future release. See https://github.com/PowerShell/DSC/issues/1429 for more details." +secureStringMustBeString = "Secure string parameter '%{name}' must be a string" +secureObjectMustBeObject = "Secure object parameter '%{name}' must be an object" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" @@ -531,6 +535,8 @@ keyNotInt = "Parameter '%{key}' is not an integer" keyNotBool = "Parameter '%{key}' is not a boolean" keyNotObject = "Parameter '%{key}' is not an object" keyNotArray = "Parameter '%{key}' is not an array" +keyNotSecureString = "Parameter '%{key}' is not a secure string" +keyNotSecureObject = "Parameter '%{key}' is not a secure object" keyNotFound = "Parameter '%{key}' not found in context" [functions.parseCidr] @@ -591,6 +597,7 @@ multipleSecrets = "Multiple secrets with the same name '%{name}' and different v extensionReturnedError = "Extension '%{extension}': %{error}" noExtensions = "No extensions supporting secrets was found" secretNotFound = "Secret '%{name}' not found" +invalidSecretFormat = "Invalid secret format returned for secret '%{name}'" [functions.shallowMerge] description = "Combines an array of objects where only the top-level objects are merged" diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index e1ebf51ff..d34e682ba 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -3,7 +3,7 @@ use crate::configure::config_doc::{ExecutionInformation, ResourceDirective}; use crate::configure::context::{Context, ProcessMode}; -use crate::configure::parameters::import_parameters; +use crate::configure::parameters::{SecureObject, SecureString, import_parameters}; use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, ResourceDiscoveryMode, RestartRequired, ValueOrCopy}}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; @@ -1048,6 +1048,29 @@ impl Configurator { // TODO: additional array constraints // TODO: object constraints + let value = match &constraint.parameter_type { + DataType::SecureString => { + if let Some(string_value) = value.as_str() { + let secure_string = SecureString { + secure_string: string_value.to_string(), + }; + serde_json::to_value(secure_string)? + } else { + return Err(DscError::Validation(t!("configure.mod.secureStringMustBeString", name = name).to_string())); + } + }, + DataType::SecureObject => { + if let Some(object_value) = value.as_object() { + let secure_object = SecureObject { + secure_object: serde_json::to_value(object_value)?, + }; + serde_json::to_value(secure_object)? + } else { + return Err(DscError::Validation(t!("configure.mod.secureObjectMustBeObject", name = name).to_string())); + } + }, + _ => value.clone(), + }; validate_parameter_type(&name, &value, &constraint.parameter_type)?; if constraint.parameter_type == DataType::SecureString || constraint.parameter_type == DataType::SecureObject { info!("{}", t!("configure.mod.setSecureParameter", name = name)); @@ -1081,27 +1104,37 @@ impl Configurator { debug!("{}", t!("configure.mod.processingParameter", name = name)); if let Some(default_value) = ¶meter.default_value { debug!("{}", t!("configure.mod.setDefaultParameter", name = name)); - let value_result = if default_value.is_string() { + let mut value = if default_value.is_string() { if let Some(value) = default_value.as_str() { self.context.process_mode = ProcessMode::ParametersDefault; - let result = self.statement_parser.parse_and_execute(value, &self.context); + let result = self.statement_parser.parse_and_execute(value, &self.context)?; self.context.process_mode = ProcessMode::Normal; result } else { return Err(DscError::Parser(t!("configure.mod.defaultStringNotDefined").to_string())); } } else { - Ok(default_value.clone()) + default_value.clone() }; - if let Ok(value) = value_result { - check_length(name, &value, parameter)?; - check_allowed_values(name, &value, parameter)?; - check_number_limits(name, &value, parameter)?; - validate_parameter_type(name, &value, ¶meter.parameter_type)?; - self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone())); - resolved_in_this_pass.push(name.clone()); + if parameter.parameter_type == DataType::SecureString && value.is_string() { + let secure_string = SecureString { + secure_string: value.as_str().unwrap().to_string(), + }; + value = serde_json::to_value(secure_string)?; + } else if parameter.parameter_type == DataType::SecureObject && value.is_object() { + let secure_object = SecureObject { + secure_object: value.clone(), + }; + value = serde_json::to_value(secure_object)?; } + + check_length(name, &value, parameter)?; + check_allowed_values(name, &value, parameter)?; + check_number_limits(name, &value, parameter)?; + validate_parameter_type(name, &value, ¶meter.parameter_type)?; + self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone())); + resolved_in_this_pass.push(name.clone()); } else { resolved_in_this_pass.push(name.clone()); } @@ -1375,7 +1408,12 @@ pub fn invoke_property_expressions(parser: &mut Statement, context: &Context, pr /// pub fn validate_parameter_type(name: &str, value: &Value, parameter_type: &DataType) -> Result<(), DscError> { match parameter_type { - DataType::String | DataType::SecureString => { + DataType::SecureString => { + if serde_json::from_value::(value.clone()).is_err() { + return Err(DscError::Validation(t!("configure.mod.parameterNotSecureString", name = name).to_string())); + } + } + DataType::String => { if !value.is_string() { return Err(DscError::Validation(t!("configure.mod.parameterNotString", name = name).to_string())); } @@ -1395,7 +1433,12 @@ pub fn validate_parameter_type(name: &str, value: &Value, parameter_type: &DataT return Err(DscError::Validation(t!("configure.mod.parameterNotArray", name = name).to_string())); } }, - DataType::Object | DataType::SecureObject => { + DataType::SecureObject => { + if serde_json::from_value::(value.clone()).is_err() { + return Err(DscError::Validation(t!("configure.mod.parameterNotSecureObject", name = name).to_string())); + } + }, + DataType::Object => { if !value.is_object() { return Err(DscError::Validation(t!("configure.mod.parameterNotObject", name = name).to_string())); } diff --git a/lib/dsc-lib/src/extensions/secret.rs b/lib/dsc-lib/src/extensions/secret.rs index bd9892e98..b73e09482 100644 --- a/lib/dsc-lib/src/extensions/secret.rs +++ b/lib/dsc-lib/src/extensions/secret.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use crate::{ + configure::parameters::SecureString, dscerror::DscError, dscresources::{ command_resource::invoke_command, @@ -100,7 +101,10 @@ impl DscExtension { // remove any trailing newline characters stdout.trim_end_matches('\n').to_string() }; - Ok(Some(secret)) + let secure_string = SecureString { + secure_string: secret.clone(), + }; + Ok(Some(serde_json::to_string(&secure_string)?)) } } else { Err(DscError::UnsupportedCapability( diff --git a/lib/dsc-lib/src/functions/parameters.rs b/lib/dsc-lib/src/functions/parameters.rs index f2dffab9d..fdc2ac0e0 100644 --- a/lib/dsc-lib/src/functions/parameters.rs +++ b/lib/dsc-lib/src/functions/parameters.rs @@ -37,19 +37,16 @@ impl Function for Parameters { // if secureString or secureObject types, we keep it as JSON object match data_type { DataType::SecureString => { - let Some(value) = value.as_str() else { - return Err(DscError::Parser(t!("functions.parameters.keyNotString", key = key).to_string())); - }; - let secure_string = SecureString { - secure_string: value.to_string(), + if serde_json::from_value::(value.clone()).is_err() { + return Err(DscError::Parser(t!("functions.parameters.keyNotSecureString", key = key).to_string())); }; - return Ok(serde_json::to_value(secure_string)?); + return Ok(value.clone()); }, DataType::SecureObject => { - let secure_object = SecureObject { - secure_object: value.clone(), + if serde_json::from_value::(value.clone()).is_err() { + return Err(DscError::Parser(t!("functions.parameters.keyNotSecureObject", key = key).to_string())); }; - return Ok(serde_json::to_value(secure_object)?); + return Ok(value.clone()); }, DataType::String => { let Some(_value) = value.as_str() else { diff --git a/lib/dsc-lib/src/functions/secret.rs b/lib/dsc-lib/src/functions/secret.rs index 9b5205232..3b53ecc9e 100644 --- a/lib/dsc-lib/src/functions/secret.rs +++ b/lib/dsc-lib/src/functions/secret.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::DscError; -use crate::configure::context::Context; +use crate::configure::{context::Context, parameters::SecureString}; use crate::extensions::dscextension::Capability; use crate::functions::{FunctionArgKind, FunctionCategory, FunctionMetadata}; use rust_i18n::t; @@ -57,7 +57,6 @@ impl Function for Secret { if secret_returned && result != secret_value { return Err(DscError::Function("secret".to_string(), t!("functions.secret.multipleSecrets", name = secret_name.clone()).to_string())); } - result = secret_value; secret_returned = true; } @@ -68,7 +67,8 @@ impl Function for Secret { } } if secret_returned { - Ok(Value::String(result)) + let secure_string = serde_json::from_str::(&result).map_err(|_err| DscError::Function("secret".to_string(), t!("functions.secret.invalidSecretFormat", name = secret_name.clone()).to_string()))?; + Ok(serde_json::to_value(secure_string)?) } else { Err(DscError::Function("secret".to_string(), t!("functions.secret.secretNotFound", name = secret_name).to_string())) } diff --git a/lib/dsc-lib/src/parser/expressions.rs b/lib/dsc-lib/src/parser/expressions.rs index 0b5996411..7fa08d034 100644 --- a/lib/dsc-lib/src/parser/expressions.rs +++ b/lib/dsc-lib/src/parser/expressions.rs @@ -7,7 +7,7 @@ use tracing::{debug, trace, warn}; use tree_sitter::Node; use crate::configure::context::Context; -use crate::configure::parameters::is_secure_value; +use crate::configure::parameters::{SecureObject, SecureString, is_secure_value}; use crate::dscerror::DscError; use crate::functions::FunctionDispatcher; use crate::parser::functions::Function; @@ -241,16 +241,20 @@ impl Expression { /// /// The converted JSON value. 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 { + let secure_string = SecureString { secure_string: string.to_string(), }; return serde_json::to_value(secure_string).unwrap_or(value.clone()); } - if let Some(obj) = value.as_object() { - let secure_object = crate::configure::parameters::SecureObject { - secure_object: serde_json::Value::Object(obj.clone()), + if value.as_object().is_some() { + let secure_object = SecureObject { + secure_object: value.clone(), }; return serde_json::to_value(secure_object).unwrap_or(value.clone()); } diff --git a/resources/dscecho/src/main.rs b/resources/dscecho/src/main.rs index ed040dc73..f46f14b5f 100644 --- a/resources/dscecho/src/main.rs +++ b/resources/dscecho/src/main.rs @@ -9,7 +9,7 @@ use clap::Parser; use rust_i18n::{i18n, t}; use schemars::schema_for; use serde_json::{Map, Value}; -use crate::echo::{Echo, Output}; +use crate::echo::{Echo, Output, SecureObject, SecureString}; i18n!("locales", fallback = "en-us"); @@ -25,26 +25,31 @@ fn main() { std::process::exit(1); } }; - if echo.show_secrets != Some(true) { - match echo.output { - Output::SecureString(_) | Output::SecureObject(_) => { + match echo.output { + Output::SecureString(s) => { + if echo.show_secrets == Some(true) { + echo.output = Output::String(s.secure_string); + } else { 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::SecureObject(o) => { + if echo.show_secrets == Some(true) { + echo.output = Output::Object(o.secure_object); + } else { + echo.output = Output::String(SECURE_VALUE_REDACTED.to_string()); + } + }, + Output::Array(ref mut arr) => { + for item in arr.iter_mut() { + if echo.show_secrets == Some(true) { + *item = get_secure_contents(item); + } else { + *item = redact(item); } - }, - Output::Object(ref mut obj) => { - obj.clone_from(redact(&Value::Object(obj.clone())) - .as_object() - .expect("Expected redact() to return a Value::Object")); }, - _ => {} - } + } + }, + _ => {} } let json = serde_json::to_string(&echo).unwrap(); println!("{json}"); @@ -66,7 +71,17 @@ fn is_secure_value(value: &Value) -> bool { false } -pub fn redact(value: &Value) -> Value { +fn get_secure_contents(value: &Value) -> Value { + if let Ok(secure_string) = serde_json::from_value::(value.clone()) { + return Value::String(secure_string.secure_string); + } else if let Ok(secure_object) = serde_json::from_value::(value.clone()) { + return Value::Object(secure_object.secure_object); + } + + value.clone() +} + +fn redact(value: &Value) -> Value { if is_secure_value(value) { return Value::String(SECURE_VALUE_REDACTED.to_string()); }