Skip to content

Commit b1306bd

Browse files
authored
Merge pull request #1127 from SteveL-MSFT/secure-before
Fix how `secureString` and `secureObject` is propagated
2 parents e90df5f + aa67d45 commit b1306bd

File tree

14 files changed

+339
-48
lines changed

14 files changed

+339
-48
lines changed

dsc/examples/secure_parameters.dsc.yaml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,27 @@ parameters:
44
type: secureString
55
myObject:
66
type: secureObject
7+
showSecrets:
8+
type: bool
9+
defaultValue: false
710
resources:
8-
- name: Echo 1
11+
- name: SecureString
912
type: Microsoft.DSC.Debug/Echo
1013
properties:
1114
output: "[parameters('myString')]"
12-
- name: Echo 2
15+
showSecrets: "[parameters('showSecrets')]"
16+
- name: SecureObject
1317
type: Microsoft.DSC.Debug/Echo
1418
properties:
1519
output: "[parameters('myObject').myProperty]"
20+
showSecrets: "[parameters('showSecrets')]"
21+
- name: SecureArray
22+
type: Microsoft.DSC.Debug/Echo
23+
properties:
24+
output: "[parameters('myObject').myArray]"
25+
showSecrets: "[parameters('showSecrets')]"
26+
- name: SecureArrayIndexed
27+
type: Microsoft.DSC.Debug/Echo
28+
properties:
29+
output: "[parameters('myObject').myArray[1]]"
30+
showSecrets: "[parameters('showSecrets')]"

dsc/examples/secure_parameters.parameters.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ parameters:
22
myString: mySecret
33
myObject:
44
myProperty: mySecretProperty
5+
myArray:
6+
- item1
7+
- item2
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
parameters:
2+
myString: mySecret
3+
myObject:
4+
myProperty: mySecretProperty
5+
myArray:
6+
- item1
7+
- item2
8+
showSecrets: true

dsc/tests/dsc_extension_secret.tests.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,12 @@ Describe 'Tests for the secret() function and extensions' {
144144
type: Microsoft.DSC.Debug/Echo
145145
properties:
146146
output: "[parameters('myString')]"
147+
showSecrets: true
147148
'@
148149
$out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
149150
$LASTEXITCODE | Should -Be 0
150151
$out.results.Count | Should -Be 1
151-
$out.results[0].result.actualState.Output | Should -BeExactly 'Hello'
152+
$out.results[0].result.actualState.Output.secureString | Should -BeExactly 'Hello'
152153
}
153154

154155
It 'Allows to pass in secret() through variables' {

dsc/tests/dsc_parameters.tests.ps1

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,61 @@ Describe 'Parameters tests' {
274274
$out.results[0].result.inDesiredState | Should -BeTrue
275275
}
276276

277-
It 'secure types can be passed as objects to resources' {
278-
$out = dsc config -f $PSScriptRoot/../examples/secure_parameters.parameters.yaml get -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json
277+
It 'secure types can be passed as objects to resources but redacted in output: <operation> <property>' -TestCases @(
278+
@{ operation = 'get'; property = 'actualState' }
279+
@{ operation = 'set'; property = 'beforeState' }
280+
@{ operation = 'set'; property = 'afterState' }
281+
@{ operation = 'test'; property = 'desiredState' }
282+
@{ operation = 'test'; property = 'actualState' }
283+
@{ operation = 'export'; property = $null }
284+
) {
285+
param($operation, $property)
286+
287+
$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
279288
$LASTEXITCODE | Should -Be 0
280-
$out.results[0].result.actualState.output | Should -BeExactly 'mySecret'
281-
$out.results[1].result.actualState.output | Should -BeExactly 'mySecretProperty'
289+
if ($operation -eq 'export') {
290+
$out.resources.Count | Should -Be 4
291+
$out.resources[0].properties.output | Should -BeExactly '<secureValue>'
292+
$out.resources[1].properties.output | Should -BeExactly '<secureValue>'
293+
$out.resources[2].properties.output[0] | Should -BeExactly '<secureValue>'
294+
$out.resources[2].properties.output[1] | Should -BeExactly '<secureValue>'
295+
$out.resources[3].properties.output | Should -BeExactly '<secureValue>'
296+
} else {
297+
$out.results.Count | Should -Be 4 -Because ($out | ConvertTo-Json -Dep 10 | Out-String)
298+
$out.results[0].result.$property.output | Should -BeExactly '<secureValue>' -Because ($out | ConvertTo-Json -Dep 10 | Out-String)
299+
$out.results[1].result.$property.output | Should -BeExactly '<secureValue>'
300+
$out.results[2].result.$property.output[0] | Should -BeExactly '<secureValue>'
301+
$out.results[2].result.$property.output[1] | Should -BeExactly '<secureValue>'
302+
$out.results[3].result.$property.output | Should -BeExactly '<secureValue>'
303+
}
304+
}
305+
306+
It 'secure types can be passed as objects to resources: <operation> <property>' -TestCases @(
307+
# `set` beforeState is redacted in output, `test` desiredState is redacted in output so those test cases are not included here
308+
@{ operation = 'get'; property = 'actualState' }
309+
@{ operation = 'set'; property = 'afterState' }
310+
@{ operation = 'test'; property = 'actualState' }
311+
@{ operation = 'export'; property = $null }
312+
) {
313+
param($operation, $property)
314+
315+
$out = dsc config -f $PSScriptRoot/../examples/secure_parameters_shown.parameters.yaml $operation -f $PSScriptRoot/../examples/secure_parameters.dsc.yaml | ConvertFrom-Json
316+
$LASTEXITCODE | Should -Be 0
317+
if ($operation -eq 'export') {
318+
$out.resources.Count | Should -Be 4 -Because ($out | ConvertTo-Json -Dep 10 | Out-String)
319+
$out.resources[0].properties.output.secureString | Should -BeExactly 'mySecret'
320+
$out.resources[1].properties.output.secureString | Should -BeExactly 'mySecretProperty'
321+
$out.resources[2].properties.output[0].secureString | Should -BeExactly 'item1'
322+
$out.resources[2].properties.output[1].secureString | Should -BeExactly 'item2'
323+
$out.resources[3].properties.output.secureObject.secureString | Should -BeExactly 'item2'
324+
} else {
325+
$out.results.Count | Should -Be 4
326+
$out.results[0].result.$property.output.secureString | Should -BeExactly 'mySecret' -Because ($out | ConvertTo-Json -Dep 10 | Out-String)
327+
$out.results[1].result.$property.output.secureString | Should -BeExactly 'mySecretProperty'
328+
$out.results[2].result.$property.output[0].secureString | Should -BeExactly 'item1'
329+
$out.results[2].result.$property.output[1].secureString | Should -BeExactly 'item2'
330+
$out.results[3].result.$property.output.secureObject.secureString | Should -BeExactly 'item2'
331+
}
282332
}
283333

284334
It 'parameter types are validated for <type>' -TestCases @(

dsc_lib/locales/en-us.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,7 @@ parsingIndexAccessor = "Parsing index accessor '%{index}'"
544544
indexNotFound = "Index value not found"
545545
invalidAccessorKind = "Invalid accessor kind: '%{kind}'"
546546
functionResult = "Function results: %{results}"
547+
functionResultSecure = "Function result is secure"
547548
evalAccessors = "Evaluating accessors"
548549
memberNameNotFound = "Member '%{member}' not found"
549550
accessOnNonObject = "Member access on non-object value"

dsc_lib/src/configure/parameters.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,63 @@
44
use schemars::JsonSchema;
55
use serde::{Deserialize, Serialize};
66
use serde_json::Value;
7-
use std::collections::HashMap;
7+
use std::{collections::HashMap, fmt::Display};
88

99
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
1010
pub struct Input {
1111
pub parameters: HashMap<String, Value>,
1212
}
1313

14+
pub const SECURE_VALUE_REDACTED: &str = "<secureValue>";
15+
16+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
17+
pub struct SecureString {
18+
#[serde(rename = "secureString")]
19+
pub secure_string: String,
20+
}
21+
22+
impl Display for SecureString {
23+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24+
write!(f, "{SECURE_VALUE_REDACTED}")
25+
}
26+
}
27+
28+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
29+
pub struct SecureObject {
30+
#[serde(rename = "secureObject")]
31+
pub secure_object: Value,
32+
}
33+
34+
impl Display for SecureObject {
35+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36+
write!(f, "{SECURE_VALUE_REDACTED}")
37+
}
38+
}
39+
40+
/// Check if a given JSON value is a secure value (either `SecureString` or `SecureObject`).
41+
///
42+
/// # Arguments
43+
///
44+
/// * `value` - The JSON value to check.
45+
///
46+
/// # Returns
47+
///
48+
/// `true` if the value is a secure value, `false` otherwise.
49+
#[must_use]
50+
pub fn is_secure_value(value: &Value) -> bool {
51+
if let Some(obj) = value.as_object() {
52+
if obj.len() == 1 && (obj.contains_key("secureString") || obj.contains_key("secureObject")) {
53+
return true;
54+
}
55+
}
56+
false
57+
}
58+
1459
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
1560
#[serde(deny_unknown_fields, untagged)]
1661
pub enum SecureKind {
1762
#[serde(rename = "secureString")]
18-
SecureString(String),
63+
SecureString(SecureString),
1964
#[serde(rename = "secureObject")]
20-
SecureObject(Value),
65+
SecureObject(SecureObject),
2166
}

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde_json::{Map, Value};
99
use std::{collections::HashMap, env, process::Stdio};
1010
use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}};
1111
use crate::dscerror::DscError;
12-
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}};
12+
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}};
1313
use tracing::{error, warn, info, debug, trace};
1414
use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command};
1515

@@ -120,8 +120,9 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
120120
};
121121

122122
if in_desired_state && execution_type == &ExecutionKind::Actual {
123+
let before_state = redact(&serde_json::from_str(desired)?);
123124
return Ok(SetResult::Resource(ResourceSetResponse{
124-
before_state: serde_json::from_str(desired)?,
125+
before_state,
125126
after_state: actual_state,
126127
changed_properties: None,
127128
}));
@@ -152,7 +153,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
152153
else {
153154
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
154155
};
155-
let pre_state = if pre_state_value.is_object() {
156+
let mut pre_state = if pre_state_value.is_object() {
156157
let mut pre_state_map: Map<String, Value> = serde_json::from_value(pre_state_value)?;
157158

158159
// 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
200201

201202
// for changed_properties, we compare post state to pre state
202203
let diff_properties = get_diff( &actual_value, &pre_state);
204+
pre_state = redact(&pre_state);
203205
Ok(SetResult::Resource(ResourceSetResponse{
204206
before_state: pre_state,
205207
after_state: actual_value,
@@ -241,6 +243,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
241243
}
242244
};
243245
let diff_properties = get_diff( &actual_state, &pre_state);
246+
pre_state = redact(&pre_state);
244247
Ok(SetResult::Resource(ResourceSetResponse {
245248
before_state: pre_state,
246249
after_state: actual_state,
@@ -286,7 +289,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
286289
return Ok(TestResult::Group(group_test_response));
287290
}
288291

289-
let expected_value: Value = serde_json::from_str(expected)?;
292+
let mut expected_value: Value = serde_json::from_str(expected)?;
290293
match test.returns {
291294
Some(ReturnKind::State) => {
292295
let actual_value: Value = match serde_json::from_str(&stdout){
@@ -297,6 +300,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
297300
};
298301
let in_desired_state = get_desired_state(&actual_value)?;
299302
let diff_properties = get_diff(&expected_value, &actual_value);
303+
expected_value = redact(&expected_value);
300304
Ok(TestResult::Resource(ResourceTestResponse {
301305
desired_state: expected_value,
302306
actual_state: actual_value,
@@ -315,6 +319,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
315319
return Err(DscError::Command(resource.resource_type.clone(), exit_code, t!("dscresources.commandResource.testNoDiff").to_string()));
316320
};
317321
let diff_properties: Vec<String> = serde_json::from_str(diff_properties)?;
322+
expected_value = redact(&expected_value);
318323
let in_desired_state = get_desired_state(&actual_value)?;
319324
Ok(TestResult::Resource(ResourceTestResponse {
320325
desired_state: expected_value,
@@ -339,6 +344,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
339344
}
340345
};
341346
let diff_properties = get_diff( &expected_value, &actual_state);
347+
expected_value = redact(&expected_value);
342348
Ok(TestResult::Resource(ResourceTestResponse {
343349
desired_state: expected_value,
344350
actual_state,
@@ -717,9 +723,9 @@ async fn run_process_async(executable: &str, args: Option<Vec<String>>, input: O
717723
#[allow(clippy::implicit_hasher)]
718724
#[tokio::main]
719725
pub async fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option<&str>, cwd: Option<&str>, env: Option<HashMap<String, String>>, exit_codes: Option<&HashMap<i32, String>>) -> Result<(i32, String, String), DscError> {
720-
debug!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
726+
trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
721727
if let Some(cwd) = cwd {
722-
debug!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd));
728+
trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd));
723729
}
724730

725731
match run_process_async(executable, args, input, cwd, env, exit_codes).await {

dsc_lib/src/dscresources/dscresource.rs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::{configure::{config_doc::{Configuration, ExecutionKind, Resource}, Configurator, context::ProcessMode}, dscresources::resource_manifest::Kind};
4+
use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::Kind};
55
use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse};
66
use dscerror::DscError;
77
use jsonschema::Validator;
@@ -311,7 +311,7 @@ impl Invoke for DscResource {
311311
let TestResult::Resource(ref resource_result) = result.results[0].result else {
312312
return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "test", resource = self.type_name).to_string()));
313313
};
314-
let desired_state = resource_result.desired_state
314+
let mut desired_state = resource_result.desired_state
315315
.as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "desiredState", property_type = "object").to_string()))?
316316
.get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))?
317317
.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 {
324324
.as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))?
325325
.get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone();
326326
let diff_properties = get_diff(&desired_state, &actual_state);
327+
desired_state = redact(&desired_state);
327328
let test_result = TestResult::Resource(ResourceTestResponse {
328329
desired_state,
329330
actual_state,
@@ -346,7 +347,7 @@ impl Invoke for DscResource {
346347
let resource_manifest = import_manifest(manifest.clone())?;
347348
if resource_manifest.test.is_none() {
348349
let get_result = self.get(expected)?;
349-
let desired_state = serde_json::from_str(expected)?;
350+
let mut desired_state = serde_json::from_str(expected)?;
350351
let actual_state = match get_result {
351352
GetResult::Group(results) => {
352353
let mut result_array: Vec<Value> = Vec::new();
@@ -360,6 +361,7 @@ impl Invoke for DscResource {
360361
}
361362
};
362363
let diff_properties = get_diff( &desired_state, &actual_state);
364+
desired_state = redact(&desired_state);
363365
let test_result = TestResult::Resource(ResourceTestResponse {
364366
desired_state,
365367
actual_state,
@@ -491,6 +493,36 @@ pub fn get_well_known_properties() -> HashMap<String, Value> {
491493
])
492494
}
493495

496+
/// Checks if the JSON value is sensitive and should be redacted
497+
///
498+
/// # Arguments
499+
///
500+
/// * `value` - The JSON value to check
501+
///
502+
/// # Returns
503+
///
504+
/// Original value if not sensitive, otherwise a redacted value
505+
pub fn redact(value: &Value) -> Value {
506+
if is_secure_value(value) {
507+
return Value::String(SECURE_VALUE_REDACTED.to_string());
508+
}
509+
510+
if let Some(map) = value.as_object() {
511+
let mut new_map = Map::new();
512+
for (key, val) in map {
513+
new_map.insert(key.clone(), redact(val));
514+
}
515+
return Value::Object(new_map);
516+
}
517+
518+
if let Some(array) = value.as_array() {
519+
let new_array: Vec<Value> = array.iter().map(redact).collect();
520+
return Value::Array(new_array);
521+
}
522+
523+
value.clone()
524+
}
525+
494526
#[must_use]
495527
/// Performs a comparison of two JSON Values if the expected is a strict subset of the actual
496528
///
@@ -524,6 +556,11 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec<String> {
524556
}
525557

526558
for (key, value) in &*map {
559+
if is_secure_value(value) {
560+
// skip secure values as they are not comparable
561+
continue;
562+
}
563+
527564
if value.is_object() {
528565
let sub_diff = get_diff(value, &actual[key]);
529566
if !sub_diff.is_empty() {

0 commit comments

Comments
 (0)