Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dsc/examples/secure_parameters.dsc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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')]"
5 changes: 5 additions & 0 deletions dsc/examples/secure_parameters_shown.parameters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
myString: mySecret
myObject:
myProperty: mySecretProperty
showSecrets: true
3 changes: 2 additions & 1 deletion dsc/tests/dsc_extension_secret.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down
13 changes: 10 additions & 3 deletions dsc/tests/dsc_parameters.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<secureValue>'
$out.results[1].result.actualState.output | Should -BeExactly '<secureValue>'
}

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 <type>' -TestCases @(
Expand Down
1 change: 1 addition & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
49 changes: 46 additions & 3 deletions dsc_lib/src/configure/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,61 @@
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 {
pub parameters: HashMap<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct SecureString {
#[serde(rename = "secureString")]
pub secure_string: String,
}

impl Display for SecureString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<secureValue>")
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct SecureObject {
#[serde(rename = "secureObject")]
pub secure_object: Value,
}

impl Display for SecureObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "<secureValue>")
}
}

/// 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(String),
SecureString(SecureString),
#[serde(rename = "secureObject")]
SecureObject(Value),
SecureObject(SecureObject),
}
11 changes: 7 additions & 4 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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){
Expand All @@ -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,
Expand All @@ -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<String> = 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,
Expand All @@ -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,
Expand Down Expand Up @@ -717,9 +720,9 @@ async fn run_process_async(executable: &str, args: Option<Vec<String>>, input: O
#[allow(clippy::implicit_hasher)]
#[tokio::main]
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> {
debug!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?}));
Comment thread
SteveL-MSFT marked this conversation as resolved.
Resolved
if let Some(cwd) = cwd {
debug!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd));
trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd));
Comment thread
SteveL-MSFT marked this conversation as resolved.
Resolved
}

match run_process_async(executable, args, input, cwd, env, exit_codes).await {
Expand Down
44 changes: 41 additions & 3 deletions dsc_lib/src/dscresources/dscresource.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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]
Expand All @@ -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,
Expand All @@ -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<Value> = Vec::new();
Expand All @@ -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,
Expand Down Expand Up @@ -491,6 +493,37 @@ pub fn get_well_known_properties() -> HashMap<String, Value> {
])
}

/// 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}");
Comment thread
SteveL-MSFT marked this conversation as resolved.
Resolved
if is_secure_value(value) {
return Value::String("<secureValue>".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<Value> = array.iter().map(redact).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
///
Expand Down Expand Up @@ -524,6 +557,11 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec<String> {
}

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() {
Expand Down
10 changes: 7 additions & 3 deletions dsc_lib/src/functions/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)?)
},
_ => {
Expand Down
Loading
Loading