From 9750c7eac7c15ed73149f4fd1903032a37be1e33 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:05:41 -0700 Subject: [PATCH 01/17] Add warning on Windows if files aren't authenticode signed --- dsc/Cargo.lock | 65 ++++++++++- dsc/src/util.rs | 5 + dsc/tests/dsc_security.tests.ps1 | 22 ++++ dsc_lib/Cargo.lock | 55 +++++++++ dsc_lib/Cargo.toml | 8 ++ dsc_lib/src/discovery/mod.rs | 15 ++- dsc_lib/src/dscerror.rs | 6 + dsc_lib/src/dscresources/command_resource.rs | 8 +- dsc_lib/src/lib.rs | 1 + dsc_lib/src/security/authenticode.rs | 112 +++++++++++++++++++ dsc_lib/src/security/mod.rs | 53 +++++++++ tools/test_group_resource/Cargo.lock | 55 +++++++++ 12 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 dsc/tests/dsc_security.tests.ps1 create mode 100644 dsc_lib/src/security/authenticode.rs create mode 100644 dsc_lib/src/security/mod.rs diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index 34ca22ba8..c7eb5255c 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -695,6 +695,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows 0.62.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -2602,7 +2605,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.61.3", ] [[package]] @@ -3341,11 +3344,24 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections 0.3.0", + "windows-core 0.62.0", + "windows-future 0.3.0", + "windows-link 0.2.0", + "windows-numerics 0.3.0", ] [[package]] @@ -3357,6 +3373,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core 0.62.0", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -3391,7 +3416,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.0", + "windows-threading 0.2.0", ] [[package]] @@ -3438,6 +3474,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -3552,6 +3598,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/dsc/src/util.rs b/dsc/src/util.rs index f2504b8a0..b4a6c0d2b 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -3,6 +3,7 @@ use crate::args::{SchemaType, OutputFormat, TraceFormat}; use crate::resolve::Include; +use dsc_lib::security::check_file_security; use dsc_lib::{ configure::{ config_doc::{ @@ -461,6 +462,10 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_ } } } else { + if let Err(err) = check_file_security(Path::new(path)) { + warn!("{err}"); + } + // see if an extension should handle this file let mut discovery = Discovery::new(); for extension in discovery.get_extensions(&Capability::Import) { diff --git a/dsc/tests/dsc_security.tests.ps1 b/dsc/tests/dsc_security.tests.ps1 new file mode 100644 index 000000000..73650ab2c --- /dev/null +++ b/dsc/tests/dsc_security.tests.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Tests for security features' { + It 'Unsigned config file gives warning' -Skip:(!$IsWindows) { + $null = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" + } + + It 'Unsigned resource manifest gives warning' -Skip:(!$IsWindows) { + $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.dsc.resource.json' is not signed.*?" + } + + It 'Unsigned resource executable gives warning' -Skip:(!$IsWindows) { + $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.exe' is not signed.*?" + } +} diff --git a/dsc_lib/Cargo.lock b/dsc_lib/Cargo.lock index 9a4257f19..87528c1ce 100644 --- a/dsc_lib/Cargo.lock +++ b/dsc_lib/Cargo.lock @@ -460,6 +460,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows", + "windows-result", + "windows-strings", ] [[package]] @@ -2174,6 +2177,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.2.0", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -2187,6 +2212,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core", + "windows-link 0.2.0", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -2221,6 +2257,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.4.0" @@ -2308,6 +2354,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index 9c75bcb52..39f0b473f 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -49,6 +49,14 @@ tree-sitter-rust = "0.24" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } uuid = { version = "1.18", features = ["v4"] } which = "8.0" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Cryptography", + "Win32_Security_WinTrust", +] } +windows-result = "0.4" +windows-strings = "0.5" [dev-dependencies] serde_yaml = "0.9" diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index f96f94d09..164663f1e 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -6,12 +6,13 @@ pub mod discovery_trait; use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery, DiscoveryFilter}; use crate::extensions::dscextension::{Capability, DscExtension}; +use crate::security::check_file_security; use crate::{dscresources::dscresource::DscResource, progress::ProgressFormat}; use core::result::Result::Ok; use semver::{Version, VersionReq}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::Path}; use command_discovery::{CommandDiscovery, ImportedManifest}; -use tracing::error; +use tracing::{error, warn}; #[derive(Clone)] pub struct Discovery { @@ -94,7 +95,7 @@ impl Discovery { } let type_name = type_name.to_lowercase(); - if let Some(resources) = self.resources.get(&type_name) { + let resource = if let Some(resources) = self.resources.get(&type_name) { if let Some(version) = version_string { let version = fix_semver(version); if let Ok(version_req) = VersionReq::parse(&version) { @@ -119,7 +120,15 @@ impl Discovery { } } else { None + }; + + if let Some(found_resource) = &resource { + if let Err(err) = check_file_security(Path::new(&found_resource.path)) { + warn!("{err}"); + } } + + resource } /// Find resources based on the required resource types. diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index 2df370729..25138533d 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -14,6 +14,9 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.adapterNotFound"))] AdapterNotFound(String), + #[error("Authenticode: {0}")] + AuthenticodeError(String), + #[error("{t}: {0}", t = t!("dscerror.booleanConversion"))] BooleanConversion(#[from] std::str::ParseBoolError), @@ -131,6 +134,9 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.validation"))] Validation(String), + #[error("Which: {0}")] + Which(#[from] which::Error), + #[error("YAML: {0}")] Yaml(#[from] serde_yaml::Error), diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index cdbd2afb3..17b685da1 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -6,8 +6,9 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; +use which::which; use std::{collections::HashMap, env, process::Stdio}; -use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, security::check_file_security}; 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 tracing::{error, warn, info, debug, trace}; @@ -596,6 +597,11 @@ async fn run_process_async(executable: &str, args: Option>, input: O // the value is based on list result of largest of built-in adapters - WMI adapter ~500KB const INITIAL_BUFFER_CAPACITY: usize = 1024*1024; + let exe = which(executable)?; + if let Err(err) = check_file_security(&exe) { + warn!("{err}"); + } + let mut command = Command::new(executable); if input.is_some() { command.stdin(Stdio::piped()); diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index 26b53be75..f4539d428 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -20,6 +20,7 @@ pub mod parser; pub mod progress; pub mod util; pub mod schemas; +pub mod security; i18n!("locales", fallback = "en-us"); diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs new file mode 100644 index 000000000..9e6d916ce --- /dev/null +++ b/dsc_lib/src/security/authenticode.rs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dscerror::DscError; +use crate::security::{add_file_as_checked, is_file_checked}; +use std::{ + ffi::OsStr, + mem::size_of, + path::Path, + ptr::{from_ref, null_mut}, + os::windows::ffi::OsStrExt, +}; +use windows::{ + core::{PCWSTR, PWSTR, GUID}, + Win32::{ + Foundation::{ + HANDLE, + HWND, + TRUST_E_NOSIGNATURE, + TRUST_E_EXPLICIT_DISTRUST, + TRUST_E_SUBJECT_NOT_TRUSTED, + CRYPT_E_SECURITY_SETTINGS, + }, + Security::WinTrust::{ + WINTRUST_FILE_INFO, WINTRUST_DATA, + WINTRUST_DATA_0, WINTRUST_DATA_UICONTEXT, + WINTRUST_ACTION_GENERIC_VERIFY_V2, WTD_STATEACTION_CLOSE, + WTD_UI_NONE, WTD_REVOKE_NONE, WTD_CHOICE_FILE, + WTD_STATEACTION_VERIFY, WTD_SAFER_FLAG, WTD_CACHE_ONLY_URL_RETRIEVAL, + WinVerifyTrustEx, + } + } +}; +use windows_result::HRESULT; + +/// Check the Authenticode signature of a file. +/// +/// # Arguments +/// +/// * `file_path` - The path to the file to check. +/// +/// # Returns +/// +/// * `Ok(())` if the file is signed and the signature is valid. +/// * `Err(DscError)` if the file is not signed or the signature is invalid +/// +pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { + if is_file_checked(file_path) { + return Ok(()); + } + + let wintrust_file_info = WINTRUST_FILE_INFO { + cbStruct: u32::try_from(size_of::())?, + pcwszFilePath: PCWSTR(OsStr::new(file_path) + .encode_wide() + .chain(std::iter::once(0)) + .collect::>() + .as_ptr()), + hFile: HANDLE(null_mut()), + pgKnownSubject: null_mut(), + }; + + let wintrust_data_0 = WINTRUST_DATA_0 { + pFile: (&raw const wintrust_file_info).cast_mut(), + }; + + let mut wintrust_data = WINTRUST_DATA { + cbStruct: u32::try_from(size_of::())?, + pPolicyCallbackData: null_mut(), + pSIPClientData: null_mut(), + dwUIChoice: WTD_UI_NONE, + fdwRevocationChecks: WTD_REVOKE_NONE, + dwUnionChoice: WTD_CHOICE_FILE, + dwStateAction: WTD_STATEACTION_VERIFY, + hWVTStateData: HANDLE(null_mut()), + pwszURLReference: PWSTR(null_mut()), + dwProvFlags: WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL, + dwUIContext: WINTRUST_DATA_UICONTEXT(0), + pSignatureSettings: null_mut(), + Anonymous: wintrust_data_0, + }; + + let result = unsafe { + WinVerifyTrustEx( + HWND(null_mut()), + from_ref::(&WINTRUST_ACTION_GENERIC_VERIFY_V2).cast_mut(), + (&raw const wintrust_data).cast_mut(), + ) + }; + + let hresult = HRESULT(result as _); + wintrust_data.dwStateAction = WTD_STATEACTION_CLOSE; + let _ = unsafe { WinVerifyTrustEx( + HWND(null_mut()), + from_ref::(&WINTRUST_ACTION_GENERIC_VERIFY_V2).cast_mut(), + (&raw const wintrust_data).cast_mut(), + ) }; + + add_file_as_checked(file_path); + + if hresult.is_ok() { + Ok(()) + } else { + match hresult { + TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(format!("The file '{}' is not signed.", file_path.display()))), + TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is explicitly distrusted.", file_path.display()))), + TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is not trusted.", file_path.display()))), + CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' does not meet the security settings.", file_path.display()))), + _ => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' could not be verified. HRESULT: 0x{:X}", file_path.display(), hresult.0))), + } + } +} diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs new file mode 100644 index 000000000..35a8760d3 --- /dev/null +++ b/dsc_lib/src/security/mod.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use authenticode::check_authenticode; +use std::{path::Path, sync::LazyLock}; + +use crate::dscerror::DscError; + +#[cfg(windows)] +mod authenticode; + +static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(vec![])); + +fn add_file_as_checked(file_path: &Path) { + let file_str = file_path.to_string_lossy().to_string(); + let mut checked_files = CHECKED_FILES.lock().unwrap(); + if !checked_files.contains(&file_str) { + checked_files.push(file_str); + } +} + +fn is_file_checked(file_path: &Path) -> bool { + let file_str = file_path.to_string_lossy().to_string(); + let checked_files = CHECKED_FILES.lock().unwrap(); + checked_files.contains(&file_str) +} + +/// Check the security of a file. +/// +/// # Arguments +/// +/// * `file_path` - The path to the file to check. +/// +/// # Returns +/// +/// * `Ok(())` if the file passes the security checks. +/// * `Err(DscError)` if the file fails the security checks. +/// +/// # Errors +/// +/// This function will return an error if the Authenticode check fails on Windows. +pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { + #[cfg(windows)] + { + check_authenticode(file_path)?; + Ok(()) + } + #[cfg(not(windows))] + { + // On non-Windows platforms, we skip the Authenticode check. + Ok(()) + } +} diff --git a/tools/test_group_resource/Cargo.lock b/tools/test_group_resource/Cargo.lock index a1c75e90a..a34608e1f 100644 --- a/tools/test_group_resource/Cargo.lock +++ b/tools/test_group_resource/Cargo.lock @@ -460,6 +460,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows", + "windows-result", + "windows-strings", ] [[package]] @@ -2185,6 +2188,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.2.0", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -2198,6 +2223,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core", + "windows-link 0.2.0", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -2232,6 +2268,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.4.0" @@ -2319,6 +2365,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" From 7b9d745351bfad36af1e2dcbb6c4144f33a71aa9 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:17:04 -0700 Subject: [PATCH 02/17] make strings localizable --- dsc_lib/locales/en-us.toml | 7 +++++++ dsc_lib/src/security/authenticode.rs | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index c78e0fcc5..a21766dfc 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -589,6 +589,13 @@ invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{ [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" +[security.authenticode] +fileNotSigned = "File '%{file}' is not signed" +signatureExplicitlyDistrusted = "The signature for file '%{file}' is explicitly distrusted" +signatureNotTrusted = "The signature for file '%{file}' is not trusted" +signatureDoesNotMeetSecuritySettings = "The signature for file '%{file}' does not meet the security settings" +signatureCouldNotBeVerified = "The signature for file '%{file}' could not be verified. HRESULT: 0x%{hresult}" + [util] foundSetting = "Found setting '%{name}' in %{path}" notFoundSetting = "Setting '%{name}' not found in %{path}" diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 9e6d916ce..47668c498 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -3,6 +3,7 @@ use crate::dscerror::DscError; use crate::security::{add_file_as_checked, is_file_checked}; +use rust_i18n::t; use std::{ ffi::OsStr, mem::size_of, @@ -102,11 +103,11 @@ pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { Ok(()) } else { match hresult { - TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(format!("The file '{}' is not signed.", file_path.display()))), - TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is explicitly distrusted.", file_path.display()))), - TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is not trusted.", file_path.display()))), - CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' does not meet the security settings.", file_path.display()))), - _ => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' could not be verified. HRESULT: 0x{:X}", file_path.display(), hresult.0))), + TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(t!("security.authenticode.fileNotSigned", file = file_path.display()).to_string())), + TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display()).to_string())), + TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureNotTrusted", file = file_path.display()).to_string())), + CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display()).to_string())), + _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0).to_string())), } } } From c01cff6ea000386a5f4826790e144c8ebe9ca5d5 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:24:51 -0700 Subject: [PATCH 03/17] format hresult as hex --- dsc_lib/src/security/authenticode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 47668c498..27200de7c 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -107,7 +107,7 @@ pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display()).to_string())), TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureNotTrusted", file = file_path.display()).to_string())), CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display()).to_string())), - _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0).to_string())), + _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0 : {:x}).to_string())), } } } From e1493788deb6a3e1c7853969e52c7c4c9ff7d265 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:05:41 -0700 Subject: [PATCH 04/17] Add warning on Windows if files aren't authenticode signed --- dsc/Cargo.lock | 65 ++++++++++- dsc/src/util.rs | 5 + dsc/tests/dsc_security.tests.ps1 | 22 ++++ dsc_lib/Cargo.lock | 55 +++++++++ dsc_lib/Cargo.toml | 8 ++ dsc_lib/src/discovery/mod.rs | 15 ++- dsc_lib/src/dscerror.rs | 6 + dsc_lib/src/dscresources/command_resource.rs | 8 +- dsc_lib/src/lib.rs | 1 + dsc_lib/src/security/authenticode.rs | 112 +++++++++++++++++++ dsc_lib/src/security/mod.rs | 53 +++++++++ tools/test_group_resource/Cargo.lock | 55 +++++++++ 12 files changed, 396 insertions(+), 9 deletions(-) create mode 100644 dsc/tests/dsc_security.tests.ps1 create mode 100644 dsc_lib/src/security/authenticode.rs create mode 100644 dsc_lib/src/security/mod.rs diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index 34ca22ba8..c7eb5255c 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -695,6 +695,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows 0.62.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -2602,7 +2605,7 @@ dependencies = [ "ntapi", "objc2-core-foundation", "objc2-io-kit", - "windows", + "windows 0.61.3", ] [[package]] @@ -3341,11 +3344,24 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections 0.3.0", + "windows-core 0.62.0", + "windows-future 0.3.0", + "windows-link 0.2.0", + "windows-numerics 0.3.0", ] [[package]] @@ -3357,6 +3373,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core 0.62.0", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -3391,7 +3416,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.0", + "windows-threading 0.2.0", ] [[package]] @@ -3438,6 +3474,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core 0.62.0", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -3552,6 +3598,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/dsc/src/util.rs b/dsc/src/util.rs index f2504b8a0..b4a6c0d2b 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -3,6 +3,7 @@ use crate::args::{SchemaType, OutputFormat, TraceFormat}; use crate::resolve::Include; +use dsc_lib::security::check_file_security; use dsc_lib::{ configure::{ config_doc::{ @@ -461,6 +462,10 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_ } } } else { + if let Err(err) = check_file_security(Path::new(path)) { + warn!("{err}"); + } + // see if an extension should handle this file let mut discovery = Discovery::new(); for extension in discovery.get_extensions(&Capability::Import) { diff --git a/dsc/tests/dsc_security.tests.ps1 b/dsc/tests/dsc_security.tests.ps1 new file mode 100644 index 000000000..73650ab2c --- /dev/null +++ b/dsc/tests/dsc_security.tests.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Tests for security features' { + It 'Unsigned config file gives warning' -Skip:(!$IsWindows) { + $null = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" + } + + It 'Unsigned resource manifest gives warning' -Skip:(!$IsWindows) { + $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.dsc.resource.json' is not signed.*?" + } + + It 'Unsigned resource executable gives warning' -Skip:(!$IsWindows) { + $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.exe' is not signed.*?" + } +} diff --git a/dsc_lib/Cargo.lock b/dsc_lib/Cargo.lock index 9a4257f19..87528c1ce 100644 --- a/dsc_lib/Cargo.lock +++ b/dsc_lib/Cargo.lock @@ -460,6 +460,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows", + "windows-result", + "windows-strings", ] [[package]] @@ -2174,6 +2177,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.2.0", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -2187,6 +2212,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core", + "windows-link 0.2.0", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -2221,6 +2257,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.4.0" @@ -2308,6 +2354,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index 9c75bcb52..39f0b473f 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -49,6 +49,14 @@ tree-sitter-rust = "0.24" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } uuid = { version = "1.18", features = ["v4"] } which = "8.0" +windows = { version = "0.62", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Cryptography", + "Win32_Security_WinTrust", +] } +windows-result = "0.4" +windows-strings = "0.5" [dev-dependencies] serde_yaml = "0.9" diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index f96f94d09..164663f1e 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -6,12 +6,13 @@ pub mod discovery_trait; use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery, DiscoveryFilter}; use crate::extensions::dscextension::{Capability, DscExtension}; +use crate::security::check_file_security; use crate::{dscresources::dscresource::DscResource, progress::ProgressFormat}; use core::result::Result::Ok; use semver::{Version, VersionReq}; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, path::Path}; use command_discovery::{CommandDiscovery, ImportedManifest}; -use tracing::error; +use tracing::{error, warn}; #[derive(Clone)] pub struct Discovery { @@ -94,7 +95,7 @@ impl Discovery { } let type_name = type_name.to_lowercase(); - if let Some(resources) = self.resources.get(&type_name) { + let resource = if let Some(resources) = self.resources.get(&type_name) { if let Some(version) = version_string { let version = fix_semver(version); if let Ok(version_req) = VersionReq::parse(&version) { @@ -119,7 +120,15 @@ impl Discovery { } } else { None + }; + + if let Some(found_resource) = &resource { + if let Err(err) = check_file_security(Path::new(&found_resource.path)) { + warn!("{err}"); + } } + + resource } /// Find resources based on the required resource types. diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index 2df370729..25138533d 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -14,6 +14,9 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.adapterNotFound"))] AdapterNotFound(String), + #[error("Authenticode: {0}")] + AuthenticodeError(String), + #[error("{t}: {0}", t = t!("dscerror.booleanConversion"))] BooleanConversion(#[from] std::str::ParseBoolError), @@ -131,6 +134,9 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.validation"))] Validation(String), + #[error("Which: {0}")] + Which(#[from] which::Error), + #[error("YAML: {0}")] Yaml(#[from] serde_yaml::Error), diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index cdbd2afb3..17b685da1 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -6,8 +6,9 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; +use which::which; use std::{collections::HashMap, env, process::Stdio}; -use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}; +use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, security::check_file_security}; 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 tracing::{error, warn, info, debug, trace}; @@ -596,6 +597,11 @@ async fn run_process_async(executable: &str, args: Option>, input: O // the value is based on list result of largest of built-in adapters - WMI adapter ~500KB const INITIAL_BUFFER_CAPACITY: usize = 1024*1024; + let exe = which(executable)?; + if let Err(err) = check_file_security(&exe) { + warn!("{err}"); + } + let mut command = Command::new(executable); if input.is_some() { command.stdin(Stdio::piped()); diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index 26b53be75..f4539d428 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -20,6 +20,7 @@ pub mod parser; pub mod progress; pub mod util; pub mod schemas; +pub mod security; i18n!("locales", fallback = "en-us"); diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs new file mode 100644 index 000000000..9e6d916ce --- /dev/null +++ b/dsc_lib/src/security/authenticode.rs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dscerror::DscError; +use crate::security::{add_file_as_checked, is_file_checked}; +use std::{ + ffi::OsStr, + mem::size_of, + path::Path, + ptr::{from_ref, null_mut}, + os::windows::ffi::OsStrExt, +}; +use windows::{ + core::{PCWSTR, PWSTR, GUID}, + Win32::{ + Foundation::{ + HANDLE, + HWND, + TRUST_E_NOSIGNATURE, + TRUST_E_EXPLICIT_DISTRUST, + TRUST_E_SUBJECT_NOT_TRUSTED, + CRYPT_E_SECURITY_SETTINGS, + }, + Security::WinTrust::{ + WINTRUST_FILE_INFO, WINTRUST_DATA, + WINTRUST_DATA_0, WINTRUST_DATA_UICONTEXT, + WINTRUST_ACTION_GENERIC_VERIFY_V2, WTD_STATEACTION_CLOSE, + WTD_UI_NONE, WTD_REVOKE_NONE, WTD_CHOICE_FILE, + WTD_STATEACTION_VERIFY, WTD_SAFER_FLAG, WTD_CACHE_ONLY_URL_RETRIEVAL, + WinVerifyTrustEx, + } + } +}; +use windows_result::HRESULT; + +/// Check the Authenticode signature of a file. +/// +/// # Arguments +/// +/// * `file_path` - The path to the file to check. +/// +/// # Returns +/// +/// * `Ok(())` if the file is signed and the signature is valid. +/// * `Err(DscError)` if the file is not signed or the signature is invalid +/// +pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { + if is_file_checked(file_path) { + return Ok(()); + } + + let wintrust_file_info = WINTRUST_FILE_INFO { + cbStruct: u32::try_from(size_of::())?, + pcwszFilePath: PCWSTR(OsStr::new(file_path) + .encode_wide() + .chain(std::iter::once(0)) + .collect::>() + .as_ptr()), + hFile: HANDLE(null_mut()), + pgKnownSubject: null_mut(), + }; + + let wintrust_data_0 = WINTRUST_DATA_0 { + pFile: (&raw const wintrust_file_info).cast_mut(), + }; + + let mut wintrust_data = WINTRUST_DATA { + cbStruct: u32::try_from(size_of::())?, + pPolicyCallbackData: null_mut(), + pSIPClientData: null_mut(), + dwUIChoice: WTD_UI_NONE, + fdwRevocationChecks: WTD_REVOKE_NONE, + dwUnionChoice: WTD_CHOICE_FILE, + dwStateAction: WTD_STATEACTION_VERIFY, + hWVTStateData: HANDLE(null_mut()), + pwszURLReference: PWSTR(null_mut()), + dwProvFlags: WTD_SAFER_FLAG | WTD_CACHE_ONLY_URL_RETRIEVAL, + dwUIContext: WINTRUST_DATA_UICONTEXT(0), + pSignatureSettings: null_mut(), + Anonymous: wintrust_data_0, + }; + + let result = unsafe { + WinVerifyTrustEx( + HWND(null_mut()), + from_ref::(&WINTRUST_ACTION_GENERIC_VERIFY_V2).cast_mut(), + (&raw const wintrust_data).cast_mut(), + ) + }; + + let hresult = HRESULT(result as _); + wintrust_data.dwStateAction = WTD_STATEACTION_CLOSE; + let _ = unsafe { WinVerifyTrustEx( + HWND(null_mut()), + from_ref::(&WINTRUST_ACTION_GENERIC_VERIFY_V2).cast_mut(), + (&raw const wintrust_data).cast_mut(), + ) }; + + add_file_as_checked(file_path); + + if hresult.is_ok() { + Ok(()) + } else { + match hresult { + TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(format!("The file '{}' is not signed.", file_path.display()))), + TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is explicitly distrusted.", file_path.display()))), + TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is not trusted.", file_path.display()))), + CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' does not meet the security settings.", file_path.display()))), + _ => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' could not be verified. HRESULT: 0x{:X}", file_path.display(), hresult.0))), + } + } +} diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs new file mode 100644 index 000000000..35a8760d3 --- /dev/null +++ b/dsc_lib/src/security/mod.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use authenticode::check_authenticode; +use std::{path::Path, sync::LazyLock}; + +use crate::dscerror::DscError; + +#[cfg(windows)] +mod authenticode; + +static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(vec![])); + +fn add_file_as_checked(file_path: &Path) { + let file_str = file_path.to_string_lossy().to_string(); + let mut checked_files = CHECKED_FILES.lock().unwrap(); + if !checked_files.contains(&file_str) { + checked_files.push(file_str); + } +} + +fn is_file_checked(file_path: &Path) -> bool { + let file_str = file_path.to_string_lossy().to_string(); + let checked_files = CHECKED_FILES.lock().unwrap(); + checked_files.contains(&file_str) +} + +/// Check the security of a file. +/// +/// # Arguments +/// +/// * `file_path` - The path to the file to check. +/// +/// # Returns +/// +/// * `Ok(())` if the file passes the security checks. +/// * `Err(DscError)` if the file fails the security checks. +/// +/// # Errors +/// +/// This function will return an error if the Authenticode check fails on Windows. +pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { + #[cfg(windows)] + { + check_authenticode(file_path)?; + Ok(()) + } + #[cfg(not(windows))] + { + // On non-Windows platforms, we skip the Authenticode check. + Ok(()) + } +} diff --git a/tools/test_group_resource/Cargo.lock b/tools/test_group_resource/Cargo.lock index a1c75e90a..a34608e1f 100644 --- a/tools/test_group_resource/Cargo.lock +++ b/tools/test_group_resource/Cargo.lock @@ -460,6 +460,9 @@ dependencies = [ "tree-sitter-rust", "uuid", "which", + "windows", + "windows-result", + "windows-strings", ] [[package]] @@ -2185,6 +2188,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9579d0e6970fd5250aa29aba5994052385ff55cf7b28a059e484bb79ea842e42" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link 0.2.0", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90dd7a7b86859ec4cdf864658b311545ef19dbcf17a672b52ab7cefe80c336f" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.0" @@ -2198,6 +2223,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2194dee901458cb79e1148a4e9aac2b164cc95fa431891e7b296ff0b2f1d8a6" +dependencies = [ + "windows-core", + "windows-link 0.2.0", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -2232,6 +2268,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +[[package]] +name = "windows-numerics" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce3498fe0aba81e62e477408383196b4b0363db5e0c27646f932676283b43d8" +dependencies = [ + "windows-core", + "windows-link 0.2.0", +] + [[package]] name = "windows-result" version = "0.4.0" @@ -2319,6 +2365,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab47f085ad6932defa48855254c758cdd0e2f2d48e62a34118a268d8f345e118" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" From ccb48ab5ec5ed89b455fa062a4e5908d31217b15 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:17:04 -0700 Subject: [PATCH 05/17] make strings localizable --- dsc_lib/locales/en-us.toml | 7 +++++++ dsc_lib/src/security/authenticode.rs | 11 ++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index ada29a34c..b124ceb95 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -590,6 +590,13 @@ invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{ [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" +[security.authenticode] +fileNotSigned = "File '%{file}' is not signed" +signatureExplicitlyDistrusted = "The signature for file '%{file}' is explicitly distrusted" +signatureNotTrusted = "The signature for file '%{file}' is not trusted" +signatureDoesNotMeetSecuritySettings = "The signature for file '%{file}' does not meet the security settings" +signatureCouldNotBeVerified = "The signature for file '%{file}' could not be verified. HRESULT: 0x%{hresult}" + [util] foundSetting = "Found setting '%{name}' in %{path}" notFoundSetting = "Setting '%{name}' not found in %{path}" diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 9e6d916ce..47668c498 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -3,6 +3,7 @@ use crate::dscerror::DscError; use crate::security::{add_file_as_checked, is_file_checked}; +use rust_i18n::t; use std::{ ffi::OsStr, mem::size_of, @@ -102,11 +103,11 @@ pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { Ok(()) } else { match hresult { - TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(format!("The file '{}' is not signed.", file_path.display()))), - TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is explicitly distrusted.", file_path.display()))), - TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' is not trusted.", file_path.display()))), - CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' does not meet the security settings.", file_path.display()))), - _ => Err(DscError::AuthenticodeError(format!("The signature of the file '{}' could not be verified. HRESULT: 0x{:X}", file_path.display(), hresult.0))), + TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(t!("security.authenticode.fileNotSigned", file = file_path.display()).to_string())), + TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display()).to_string())), + TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureNotTrusted", file = file_path.display()).to_string())), + CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display()).to_string())), + _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0).to_string())), } } } From 7972bea492659553b4b599fb7b1ca78e97794619 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 16:24:51 -0700 Subject: [PATCH 06/17] format hresult as hex --- dsc_lib/src/security/authenticode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 47668c498..27200de7c 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -107,7 +107,7 @@ pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display()).to_string())), TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureNotTrusted", file = file_path.display()).to_string())), CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display()).to_string())), - _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0).to_string())), + _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0 : {:x}).to_string())), } } } From d6d252e0a059553a698674066e8ab1e05d6de816 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 16:53:54 -0700 Subject: [PATCH 07/17] fix conditional build --- dsc_lib/Cargo.toml | 2 ++ dsc_lib/src/security/authenticode.rs | 16 +++++++----- dsc_lib/src/security/mod.rs | 37 ++++++++++++++++------------ 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index 39f0b473f..e8e6d650a 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -49,6 +49,8 @@ tree-sitter-rust = "0.24" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } uuid = { version = "1.18", features = ["v4"] } which = "8.0" + +[target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_Security", diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 27200de7c..45921c063 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -9,8 +9,10 @@ use std::{ mem::size_of, path::Path, ptr::{from_ref, null_mut}, - os::windows::ffi::OsStrExt, }; +#[cfg(windows)] +use std::os::windows::ffi::OsStrExt; +#[cfg(windows)] use windows::{ core::{PCWSTR, PWSTR, GUID}, Win32::{ @@ -32,19 +34,21 @@ use windows::{ } } }; +#[cfg(windows)] use windows_result::HRESULT; /// Check the Authenticode signature of a file. -/// +/// /// # Arguments -/// +/// /// * `file_path` - The path to the file to check. -/// +/// /// # Returns -/// +/// /// * `Ok(())` if the file is signed and the signature is valid. /// * `Err(DscError)` if the file is not signed or the signature is invalid -/// +/// +#[cfg(windows)] pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { if is_file_checked(file_path) { return Ok(()); diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index 35a8760d3..2cc19cd59 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -1,16 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +#[cfg(windows)] use authenticode::check_authenticode; -use std::{path::Path, sync::LazyLock}; +use std::path::Path; +#[cfg(windows)] +use std::sync::LazyLock; use crate::dscerror::DscError; #[cfg(windows)] mod authenticode; +#[cfg(windows)] static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(vec![])); +#[cfg(windows)] fn add_file_as_checked(file_path: &Path) { let file_str = file_path.to_string_lossy().to_string(); let mut checked_files = CHECKED_FILES.lock().unwrap(); @@ -19,6 +24,7 @@ fn add_file_as_checked(file_path: &Path) { } } +#[cfg(windows)] fn is_file_checked(file_path: &Path) -> bool { let file_str = file_path.to_string_lossy().to_string(); let checked_files = CHECKED_FILES.lock().unwrap(); @@ -28,26 +34,25 @@ fn is_file_checked(file_path: &Path) -> bool { /// Check the security of a file. /// /// # Arguments -/// +/// /// * `file_path` - The path to the file to check. -/// +/// /// # Returns -/// +/// /// * `Ok(())` if the file passes the security checks. /// * `Err(DscError)` if the file fails the security checks. -/// +/// /// # Errors -/// +/// /// This function will return an error if the Authenticode check fails on Windows. +#[cfg(windows)] pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { - #[cfg(windows)] - { - check_authenticode(file_path)?; - Ok(()) - } - #[cfg(not(windows))] - { - // On non-Windows platforms, we skip the Authenticode check. - Ok(()) - } + check_authenticode(file_path)?; + Ok(()) +} + +/// On non-Windows platforms, this function is a no-op. +#[cfg(not(windows))] +pub fn check_file_security(_file_path: &Path) -> Result<(), DscError> { + Ok(()) } From a5800180b2c5da2083d1d8db29c097d36b3eaee1 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 17:00:51 -0700 Subject: [PATCH 08/17] fix clippy --- dsc_lib/src/security/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index 2cc19cd59..ef4842e4c 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -34,16 +34,13 @@ fn is_file_checked(file_path: &Path) -> bool { /// Check the security of a file. /// /// # Arguments -/// /// * `file_path` - The path to the file to check. /// /// # Returns -/// /// * `Ok(())` if the file passes the security checks. /// * `Err(DscError)` if the file fails the security checks. /// /// # Errors -/// /// This function will return an error if the Authenticode check fails on Windows. #[cfg(windows)] pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { @@ -52,6 +49,15 @@ pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { } /// On non-Windows platforms, this function is a no-op. +/// +/// # Arguments +/// * `_file_path` - The path to the file to check. +/// +/// # Returns +/// * `Ok(())` always, as there are no security checks on non-Windows platforms. +/// +/// # Errors +/// This function does not return any errors on non-Windows platforms. #[cfg(not(windows))] pub fn check_file_security(_file_path: &Path) -> Result<(), DscError> { Ok(()) From ccad5d3e7efda89c9beace8e4b3f6fa6c269faa5 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 17:02:11 -0700 Subject: [PATCH 09/17] remove unncessary conditions --- dsc_lib/src/security/authenticode.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode.rs index 45921c063..94040f528 100644 --- a/dsc_lib/src/security/authenticode.rs +++ b/dsc_lib/src/security/authenticode.rs @@ -10,9 +10,7 @@ use std::{ path::Path, ptr::{from_ref, null_mut}, }; -#[cfg(windows)] use std::os::windows::ffi::OsStrExt; -#[cfg(windows)] use windows::{ core::{PCWSTR, PWSTR, GUID}, Win32::{ @@ -34,7 +32,6 @@ use windows::{ } } }; -#[cfg(windows)] use windows_result::HRESULT; /// Check the Authenticode signature of a file. @@ -48,7 +45,6 @@ use windows_result::HRESULT; /// * `Ok(())` if the file is signed and the signature is valid. /// * `Err(DscError)` if the file is not signed or the signature is invalid /// -#[cfg(windows)] pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { if is_file_checked(file_path) { return Ok(()); From a1fa23ea27313b81ad481bdb08b9f2472e734cf2 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 19:43:51 -0700 Subject: [PATCH 10/17] fix tests setting trace level --- dsc/tests/dsc.exist.tests.ps1 | 8 ++++++++ dsc/tests/dsc_args.tests.ps1 | 2 ++ dsc/tests/dsc_config_get.tests.ps1 | 8 ++++++++ dsc/tests/dsc_config_set.tests.ps1 | 8 ++++++++ dsc/tests/dsc_config_test.tests.ps1 | 8 ++++++++ dsc/tests/dsc_discovery.tests.ps1 | 2 ++ dsc/tests/dsc_export.tests.ps1 | 7 +++++++ dsc/tests/dsc_expressions.tests.ps1 | 9 +++++++++ dsc/tests/dsc_extension_discover.tests.ps1 | 2 ++ dsc/tests/dsc_functions.tests.ps1 | 8 ++++++++ dsc/tests/dsc_group.tests.ps1 | 8 ++++++++ dsc/tests/dsc_include.tests.ps1 | 5 +++++ dsc/tests/dsc_mcp.tests.ps1 | 2 ++ dsc/tests/dsc_metadata.tests.ps1 | 8 ++++++++ dsc/tests/dsc_osinfo.tests.ps1 | 8 ++++++++ dsc/tests/dsc_parameters.tests.ps1 | 8 ++++++++ dsc/tests/dsc_reference.tests.ps1 | 8 ++++++++ dsc/tests/dsc_resource_get.tests.ps1 | 8 ++++++++ dsc/tests/dsc_resource_input.tests.ps1 | 2 ++ dsc/tests/dsc_resource_list.tests.ps1 | 8 ++++++++ dsc/tests/dsc_resource_set.tests.ps1 | 8 ++++++++ dsc/tests/dsc_resource_test.tests.ps1 | 8 ++++++++ dsc/tests/dsc_schema.tests.ps1 | 8 ++++++++ dsc/tests/dsc_security.tests.ps1 | 6 +++--- dsc/tests/dsc_set.tests.ps1 | 5 +++++ dsc/tests/dsc_test.tests.ps1 | 8 ++++++++ dsc/tests/dsc_variables.tests.ps1 | 8 ++++++++ dsc/tests/dsc_version.tests.ps1 | 8 ++++++++ dsc/tests/dsc_whatif.tests.ps1 | 8 ++++++++ osinfo/tests/osinfo.tests.ps1 | 8 ++++++++ .../Tests/powershellgroup.config.tests.ps1 | 2 ++ .../Tests/powershellgroup.resource.tests.ps1 | 2 ++ powershell-adapter/Tests/win_powershell_cache.tests.ps1 | 2 ++ reboot_pending/tests/reboot_pending.tests.ps1 | 2 ++ registry/tests/registry.config.set.tests.ps1 | 8 ++++++++ tools/test_group_resource/tests/provider.tests.ps1 | 7 +++++++ wmi-adapter/Tests/wmi.tests.ps1 | 2 ++ 37 files changed, 224 insertions(+), 3 deletions(-) diff --git a/dsc/tests/dsc.exist.tests.ps1 b/dsc/tests/dsc.exist.tests.ps1 index 58c0711be..69e6814c9 100644 --- a/dsc/tests/dsc.exist.tests.ps1 +++ b/dsc/tests/dsc.exist.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe '_exist tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Resource supporting exist on set should receive _exist for: ' -TestCases @( @{ exist = $true } @{ exist = $false } diff --git a/dsc/tests/dsc_args.tests.ps1 b/dsc/tests/dsc_args.tests.ps1 index de88b3db2..d448c0b69 100644 --- a/dsc/tests/dsc_args.tests.ps1 +++ b/dsc/tests/dsc_args.tests.ps1 @@ -3,6 +3,7 @@ Describe 'config argument tests' { BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' $manifest = @' { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", @@ -52,6 +53,7 @@ Describe 'config argument tests' { AfterAll { $env:DSC_RESOURCE_PATH = $oldPath + $env:DSC_TRACE_LEVEL = $null } It 'input is ' -TestCases @( diff --git a/dsc/tests/dsc_config_get.tests.ps1 b/dsc/tests/dsc_config_get.tests.ps1 index 04f0d15d0..2080fc58c 100644 --- a/dsc/tests/dsc_config_get.tests.ps1 +++ b/dsc/tests/dsc_config_get.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'dsc config get tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'can successfully get config with multiple registry resource instances: ' -Skip:(!$IsWindows) -TestCases @( @{ config = 'osinfo_registry.dsc.json' } @{ config = 'osinfo_registry.dsc.yaml' } diff --git a/dsc/tests/dsc_config_set.tests.ps1 b/dsc/tests/dsc_config_set.tests.ps1 index d9ab9703f..92b40b3cc 100644 --- a/dsc/tests/dsc_config_set.tests.ps1 +++ b/dsc/tests/dsc_config_set.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'dsc config set tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'can use _exist with resources that support and do not support it' { $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json diff --git a/dsc/tests/dsc_config_test.tests.ps1 b/dsc/tests/dsc_config_test.tests.ps1 index 7f365268d..049350621 100644 --- a/dsc/tests/dsc_config_test.tests.ps1 +++ b/dsc/tests/dsc_config_test.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'dsc config test tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Assertion works correctly' { $configYaml = @' $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 0aa03d9fd..07d5000bf 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -4,6 +4,7 @@ Describe 'tests for resource discovery' { BeforeAll { $env:DSC_RESOURCE_PATH = $testdrive + $env:DSC_TRACE_LEVEL = 'error' $script:lookupTableFilePath = if ($IsWindows) { Join-Path $env:LocalAppData "dsc\AdaptedResourcesLookupTable.json" @@ -18,6 +19,7 @@ Describe 'tests for resource discovery' { AfterAll { $env:DSC_RESOURCE_PATH = $null + $env:DSC_TRACE_LEVEL = $null } It 'Use DSC_RESOURCE_PATH instead of PATH when defined' { diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index ab7aa6f15..228e550b1 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -2,6 +2,13 @@ # Licensed under the MIT License. Describe 'resource export tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } It 'Export can be called on individual resource' { diff --git a/dsc/tests/dsc_expressions.tests.ps1 b/dsc/tests/dsc_expressions.tests.ps1 index 34d4b3e11..7f7b6d1a3 100644 --- a/dsc/tests/dsc_expressions.tests.ps1 +++ b/dsc/tests/dsc_expressions.tests.ps1 @@ -2,6 +2,15 @@ # Licensed under the MIT License. Describe 'Expressions tests' { + + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Accessors work: ' -TestCases @( @{ text = "[parameters('test').hello]"; expected = '@{world=there}' } @{ text = "[parameters('test').hello.world]"; expected = 'there' } diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index 03da8c429..67f54d97d 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -14,10 +14,12 @@ Describe 'Discover extension tests' { $oldPath = $env:PATH $toolPath = Resolve-Path -Path "$PSScriptRoot/../../extensions/test/discover" $env:PATH = "$toolPath" + [System.IO.Path]::PathSeparator + $oldPath + $env:DSC_TRACE_LEVEL = 'error' } AfterAll { $env:PATH = $oldPath + $env:DSC_TRACE_LEVEL = $null } It 'Discover extensions' { diff --git a/dsc/tests/dsc_functions.tests.ps1 b/dsc/tests/dsc_functions.tests.ps1 index 6538c9c7e..63c4928b8 100644 --- a/dsc/tests/dsc_functions.tests.ps1 +++ b/dsc/tests/dsc_functions.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'tests for function expressions' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'function works: ' -TestCases @( @{ text = "[concat('a', 'b')]"; expected = 'ab' } @{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' } diff --git a/dsc/tests/dsc_group.tests.ps1 b/dsc/tests/dsc_group.tests.ps1 index 1030b7651..b853c512a 100644 --- a/dsc/tests/dsc_group.tests.ps1 +++ b/dsc/tests/dsc_group.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Group resource tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Nested groups should work for get' { $out = (dsc config get -f $PSScriptRoot/../examples/groups.dsc.yaml -o yaml | Out-String).Trim() $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_include.tests.ps1 b/dsc/tests/dsc_include.tests.ps1 index b1b7a0b81..a739aed4b 100644 --- a/dsc/tests/dsc_include.tests.ps1 +++ b/dsc/tests/dsc_include.tests.ps1 @@ -10,6 +10,11 @@ Describe 'Include tests' { $osinfoParametersConfigPath = Get-Item (Join-Path $includePath 'osinfo.parameters.yaml') $logPath = Join-Path $TestDrive 'stderr.log' + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null } It 'Include invalid config file' { diff --git a/dsc/tests/dsc_mcp.tests.ps1 b/dsc/tests/dsc_mcp.tests.ps1 index c72261469..6afcf9d3f 100644 --- a/dsc/tests/dsc_mcp.tests.ps1 +++ b/dsc/tests/dsc_mcp.tests.ps1 @@ -11,6 +11,7 @@ Describe 'Tests for MCP server' { $processStartInfo.RedirectStandardOutput = $true $processStartInfo.RedirectStandardInput = $true $mcp = [System.Diagnostics.Process]::Start($processStartInfo) + $env:DSC_TRACE_LEVEL = 'error' function Send-McpRequest($request, [switch]$notify) { $request = $request | ConvertTo-Json -Compress -Depth 10 @@ -27,6 +28,7 @@ Describe 'Tests for MCP server' { } AfterAll { + $env:DSC_TRACE_LEVEL = $null $mcp.StandardInput.Close() $mcp.WaitForExit() } diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 4ecd5b072..0e78ad8a3 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'metadata tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'metadata not provided if not declared in resource schema' { $configYaml = @' $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json diff --git a/dsc/tests/dsc_osinfo.tests.ps1 b/dsc/tests/dsc_osinfo.tests.ps1 index 777a8be10..03806fad0 100644 --- a/dsc/tests/dsc_osinfo.tests.ps1 +++ b/dsc/tests/dsc_osinfo.tests.ps1 @@ -1,4 +1,12 @@ Describe 'Tests for osinfo examples' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Config with default parameters and get works' { $out = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml | ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index 2a2e569f0..0474dcc61 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Parameters tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Input can be provided as ' -TestCases @( @{ inputType = 'string' } @{ inputType = 'file' } diff --git a/dsc/tests/dsc_reference.tests.ps1 b/dsc/tests/dsc_reference.tests.ps1 index 6e7e83e91..12ca365ef 100644 --- a/dsc/tests/dsc_reference.tests.ps1 +++ b/dsc/tests/dsc_reference.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Tests for config using reference function' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Reference works for ' -TestCases @( @{ operation = 'get' }, @{ operation = 'test' } diff --git a/dsc/tests/dsc_resource_get.tests.ps1 b/dsc/tests/dsc_resource_get.tests.ps1 index d39be27dc..77ddee1b9 100644 --- a/dsc/tests/dsc_resource_get.tests.ps1 +++ b/dsc/tests/dsc_resource_get.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'resource get tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'should get from registry using resource' -Skip:(!$IsWindows) -TestCases @( @{ type = 'string' } ) { diff --git a/dsc/tests/dsc_resource_input.tests.ps1 b/dsc/tests/dsc_resource_input.tests.ps1 index 74f33bf79..758d05a1f 100644 --- a/dsc/tests/dsc_resource_input.tests.ps1 +++ b/dsc/tests/dsc_resource_input.tests.ps1 @@ -3,6 +3,7 @@ Describe 'tests for resource input' { BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' $manifest = @' { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", @@ -91,6 +92,7 @@ Describe 'tests for resource input' { AfterAll { $env:DSC_RESOURCE_PATH = $oldPath + $env:DSC_TRACE_LEVEL = $null } It 'Input can be sent to the resource for: ' -TestCases @( diff --git a/dsc/tests/dsc_resource_list.tests.ps1 b/dsc/tests/dsc_resource_list.tests.ps1 index 8b6e14175..7fec8c89c 100644 --- a/dsc/tests/dsc_resource_list.tests.ps1 +++ b/dsc/tests/dsc_resource_list.tests.ps1 @@ -10,6 +10,14 @@ BeforeDiscovery { } Describe 'Tests for listing resources' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'dsc resource list' { $resources = dsc resource list | ConvertFrom-Json -Depth 10 $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_resource_set.tests.ps1 b/dsc/tests/dsc_resource_set.tests.ps1 index a0c94395a..6b5c89d97 100644 --- a/dsc/tests/dsc_resource_set.tests.ps1 +++ b/dsc/tests/dsc_resource_set.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Invoke a resource set directly' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'set returns proper error code if no input is provided' { $out = dsc resource set -r Test/Version 2>&1 $LASTEXITCODE | Should -Be 1 diff --git a/dsc/tests/dsc_resource_test.tests.ps1 b/dsc/tests/dsc_resource_test.tests.ps1 index d0aae7d49..3b4576b83 100644 --- a/dsc/tests/dsc_resource_test.tests.ps1 +++ b/dsc/tests/dsc_resource_test.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Invoke a resource test directly' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'test can be called on a resource' { $os = if ($IsWindows) { 'Windows' diff --git a/dsc/tests/dsc_schema.tests.ps1 b/dsc/tests/dsc_schema.tests.ps1 index 43c3b5631..d9d494037 100644 --- a/dsc/tests/dsc_schema.tests.ps1 +++ b/dsc/tests/dsc_schema.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'config schema tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'return resource schema' -Skip:(!$IsWindows) { $schema = dsc resource schema -r Microsoft.Windows/Registry $LASTEXITCODE | Should -Be 0 diff --git a/dsc/tests/dsc_security.tests.ps1 b/dsc/tests/dsc_security.tests.ps1 index 73650ab2c..d1c2ccfa2 100644 --- a/dsc/tests/dsc_security.tests.ps1 +++ b/dsc/tests/dsc_security.tests.ps1 @@ -5,18 +5,18 @@ Describe 'Tests for security features' { It 'Unsigned config file gives warning' -Skip:(!$IsWindows) { $null = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" } It 'Unsigned resource manifest gives warning' -Skip:(!$IsWindows) { $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.dsc.resource.json' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo.dsc.resource.json' is not signed.*?" } It 'Unsigned resource executable gives warning' -Skip:(!$IsWindows) { $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: The file '.*?\\osinfo.exe' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo.exe' is not signed.*?" } } diff --git a/dsc/tests/dsc_set.tests.ps1 b/dsc/tests/dsc_set.tests.ps1 index 05d6ef115..cb7821996 100644 --- a/dsc/tests/dsc_set.tests.ps1 +++ b/dsc/tests/dsc_set.tests.ps1 @@ -3,6 +3,7 @@ Describe 'resource set tests' { BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' $manifest = @' { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", @@ -53,6 +54,10 @@ Describe 'resource set tests' { Set-Content -Path "$TestDrive/SetNoTest.dsc.resource.json" -Value $manifest } + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + BeforeEach { if ($IsWindows) { $json = @' diff --git a/dsc/tests/dsc_test.tests.ps1 b/dsc/tests/dsc_test.tests.ps1 index 52b79ddd7..b07f6bcb7 100644 --- a/dsc/tests/dsc_test.tests.ps1 +++ b/dsc/tests/dsc_test.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'resource test tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'should confirm matching state' -Skip:(!$IsWindows) { $json = @' { diff --git a/dsc/tests/dsc_variables.tests.ps1 b/dsc/tests/dsc_variables.tests.ps1 index de0d44a18..6349fcc80 100644 --- a/dsc/tests/dsc_variables.tests.ps1 +++ b/dsc/tests/dsc_variables.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'Configruation variables tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'Variables example config works' { $configFile = "$PSSCriptRoot/../examples/variables.dsc.yaml" $out = dsc config get -f $configFile | ConvertFrom-Json diff --git a/dsc/tests/dsc_version.tests.ps1 b/dsc/tests/dsc_version.tests.ps1 index b3e7a7e93..c3d27af23 100644 --- a/dsc/tests/dsc_version.tests.ps1 +++ b/dsc/tests/dsc_version.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'tests for metadata versioning' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'returns the correct dsc semantic version in metadata' { $config_yaml = @" `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json diff --git a/dsc/tests/dsc_whatif.tests.ps1 b/dsc/tests/dsc_whatif.tests.ps1 index 4e2b13571..be39bb91e 100644 --- a/dsc/tests/dsc_whatif.tests.ps1 +++ b/dsc/tests/dsc_whatif.tests.ps1 @@ -1,4 +1,12 @@ Describe 'whatif tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + AfterEach { if ($IsWindows) { Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore diff --git a/osinfo/tests/osinfo.tests.ps1 b/osinfo/tests/osinfo.tests.ps1 index b07e81ced..7edd6c931 100644 --- a/osinfo/tests/osinfo.tests.ps1 +++ b/osinfo/tests/osinfo.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'osinfo resource tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'should get osinfo' { $out = dsc resource get -r Microsoft/osInfo | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 3ebee5844..7b625d4c5 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -7,6 +7,7 @@ Describe 'PowerShell adapter resource tests' { $OldPSModulePath = $env:PSModulePath $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot $pwshConfigPath = Join-path $PSScriptRoot "class_ps_resources.dsc.yaml" + $env:DSC_TRACE_LEVEL = 'error' if ($IsLinux -or $IsMacOS) { $cacheFilePath = Join-Path $env:HOME ".dsc" "PSAdapterCache.json" @@ -17,6 +18,7 @@ Describe 'PowerShell adapter resource tests' { } AfterAll { + $env:DSC_TRACE_LEVEL = $null $env:PSModulePath = $OldPSModulePath } diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index 4fd12b4e4..eb198e35f 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -6,6 +6,7 @@ Describe 'PowerShell adapter resource tests' { BeforeAll { $OldPSModulePath = $env:PSModulePath $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot + $env:DSC_TRACE_LEVEL = 'error' if ($IsLinux -or $IsMacOS) { $cacheFilePath = Join-Path $env:HOME ".dsc" "PSAdapterCache.json" @@ -17,6 +18,7 @@ Describe 'PowerShell adapter resource tests' { AfterAll { $env:PSModulePath = $OldPSModulePath + $env:DSC_TRACE_LEVEL = $null } BeforeEach { diff --git a/powershell-adapter/Tests/win_powershell_cache.tests.ps1 b/powershell-adapter/Tests/win_powershell_cache.tests.ps1 index 8f2ca4b58..0eb68ceea 100644 --- a/powershell-adapter/Tests/win_powershell_cache.tests.ps1 +++ b/powershell-adapter/Tests/win_powershell_cache.tests.ps1 @@ -19,6 +19,7 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio $env:DSC_RESOURCE_PATH = $dscHome + [System.IO.Path]::PathSeparator + $psexeHome + [System.IO.Path]::PathSeparator + $ps7exeHome $null = winrm quickconfig -quiet -force 2>&1 $env:PSModulePath = $PSScriptRoot + [System.IO.Path]::PathSeparator + $env:PSModulePath + $env:DSC_TRACE_LEVEL = 'error' $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" @@ -30,6 +31,7 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio AfterAll { $env:PSModulePath = $OldPSModulePath $env:DSC_RESOURCE_PATH = $null + $env:DSC_TRACE_LEVEL = $null # Remove after all the tests are done Remove-Module $script:winPSModule -Force -ErrorAction Ignore diff --git a/reboot_pending/tests/reboot_pending.tests.ps1 b/reboot_pending/tests/reboot_pending.tests.ps1 index 81658ac14..4603fb3c3 100644 --- a/reboot_pending/tests/reboot_pending.tests.ps1 +++ b/reboot_pending/tests/reboot_pending.tests.ps1 @@ -16,10 +16,12 @@ Describe 'reboot_pending resource tests' -Skip:(!$IsWindows -or !$isElevated) { if (-not (Get-ItemProperty "$keyPath\$keyName" -ErrorAction Ignore)) { New-ItemProperty -Path $keyPath -Name $keyName -Value 1 -PropertyType DWord -Force | Out-Null } + $env:DSC_TRACE_LEVEL = 'error' } AfterAll { Remove-ItemProperty -Path $keyPath -Name $keyName -ErrorAction Ignore + $env:DSC_TRACE_LEVEL = $null } It 'should get reboot_pending' { diff --git a/registry/tests/registry.config.set.tests.ps1 b/registry/tests/registry.config.set.tests.ps1 index 7090be8a0..7d6b672c1 100644 --- a/registry/tests/registry.config.set.tests.ps1 +++ b/registry/tests/registry.config.set.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'registry config set tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + AfterEach { if ($IsWindows) { Remove-Item -Path 'HKCU:\1' -Recurse -ErrorAction Ignore diff --git a/tools/test_group_resource/tests/provider.tests.ps1 b/tools/test_group_resource/tests/provider.tests.ps1 index 140dd1fb5..44059b0dc 100644 --- a/tools/test_group_resource/tests/provider.tests.ps1 +++ b/tools/test_group_resource/tests/provider.tests.ps1 @@ -2,6 +2,13 @@ # Licensed under the MIT License. Describe 'Resource adapter tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } It 'Can list adapter resources' { diff --git a/wmi-adapter/Tests/wmi.tests.ps1 b/wmi-adapter/Tests/wmi.tests.ps1 index 555ce4552..d2a5daa61 100644 --- a/wmi-adapter/Tests/wmi.tests.ps1 +++ b/wmi-adapter/Tests/wmi.tests.ps1 @@ -8,6 +8,7 @@ Describe 'WMI adapter resource tests' { { $OldPSModulePath = $env:PSModulePath $env:PSModulePath += ";" + $PSScriptRoot + $env:DSC_TRACE_LEVEL = 'error' $configPath = Join-path $PSScriptRoot "test_wmi_config.dsc.yaml" } @@ -16,6 +17,7 @@ Describe 'WMI adapter resource tests' { if ($IsWindows) { $env:PSModulePath = $OldPSModulePath + $env:DSC_TRACE_LEVEL = $null } } From 7d9e25621516c1bdd9f10c27eb2a1cdaef49f4fe Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 19:55:34 -0700 Subject: [PATCH 11/17] fix merge --- dsc_lib/Cargo.toml | 8 -------- .../security/{authenticode.rs => authenticode_windows.rs} | 0 dsc_lib/src/security/mod.rs | 2 +- 3 files changed, 1 insertion(+), 9 deletions(-) rename dsc_lib/src/security/{authenticode.rs => authenticode_windows.rs} (100%) diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index 7e88dba7e..e8e6d650a 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -49,14 +49,6 @@ tree-sitter-rust = "0.24" tree-sitter-dscexpression = { path = "../tree-sitter-dscexpression" } uuid = { version = "1.18", features = ["v4"] } which = "8.0" -windows = { version = "0.62", features = [ - "Win32_Foundation", - "Win32_Security", - "Win32_Security_Cryptography", - "Win32_Security_WinTrust", -] } -windows-result = "0.4" -windows-strings = "0.5" [target.'cfg(windows)'.dependencies] windows = { version = "0.62", features = [ diff --git a/dsc_lib/src/security/authenticode.rs b/dsc_lib/src/security/authenticode_windows.rs similarity index 100% rename from dsc_lib/src/security/authenticode.rs rename to dsc_lib/src/security/authenticode_windows.rs diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index ef4842e4c..4b7253bb8 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -10,7 +10,7 @@ use std::sync::LazyLock; use crate::dscerror::DscError; #[cfg(windows)] -mod authenticode; +mod authenticode_windows; #[cfg(windows)] static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(vec![])); From ef792d1e14cee1e92247bcb017b3593e0d577476 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 20:07:33 -0700 Subject: [PATCH 12/17] fix use of renamed file --- dsc_lib/src/security/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index 4b7253bb8..ac0c1ae1f 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. #[cfg(windows)] -use authenticode::check_authenticode; +use authenticode_windows::check_authenticode; use std::path::Path; #[cfg(windows)] use std::sync::LazyLock; From 5fa6df8bc687a7820eec97f90af8b84308b34606 Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 23 Sep 2025 20:44:27 -0700 Subject: [PATCH 13/17] fix tests --- dsc/tests/dsc_discovery.tests.ps1 | 2 +- dsc/tests/dsc_metadata.tests.ps1 | 6 +++--- tools/test_group_resource/tests/provider.tests.ps1 | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 07d5000bf..75fee3f82 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -96,7 +96,7 @@ Describe 'tests for resource discovery' { try { $env:DSC_RESOURCE_PATH = $testdrive Set-Content -Path "$testdrive/test.dsc.resource.json" -Value $manifest - $null = dsc resource list 2> "$testdrive/error.txt" + $null = dsc -l warn resource list 2> "$testdrive/error.txt" "$testdrive/error.txt" | Should -FileContentMatchExactly 'WARN.*?does not use semver' -Because (Get-Content -Raw "$testdrive/error.txt") } finally { diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 0e78ad8a3..095220f3c 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -21,7 +21,7 @@ Describe 'metadata tests' { properties: output: hello world '@ - $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $out = dsc -l warn config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" $out.results.result.actualState.output | Should -BeExactly 'hello world' @@ -139,7 +139,7 @@ Describe 'metadata tests' { hello: world validOne: true '@ - $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $out = dsc -l warn config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.results.count | Should -Be 1 $out.results[0].metadata.validOne | Should -BeTrue @@ -210,7 +210,7 @@ Describe 'metadata tests' { _restartRequired: - invalid: item '@ - $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $out = dsc -l warn config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property '_restartRequired' which contains invalid value: ``[{`"invalid`":`"item`"}]*" $out.results[0].metadata._restartRequired | Should -BeNullOrEmpty diff --git a/tools/test_group_resource/tests/provider.tests.ps1 b/tools/test_group_resource/tests/provider.tests.ps1 index 44059b0dc..567f2a68e 100644 --- a/tools/test_group_resource/tests/provider.tests.ps1 +++ b/tools/test_group_resource/tests/provider.tests.ps1 @@ -63,7 +63,7 @@ Describe 'Resource adapter tests' { Set-Content -Path testdrive:/invalid.dsc.resource.json -Value $invalid_manifest $env:PATH += [System.IO.Path]::PathSeparator + (Resolve-Path (Resolve-Path $TestDrive -Relative)) - $out = dsc resource list '*invalid*' -a '*InvalidTestGroup*' 2>&1 + $out = dsc -l warn resource list '*invalid*' -a '*InvalidTestGroup*' 2>&1 $LASTEXITCODE | Should -Be 0 ,$out | Should -Match ".*?'requires'*" } From 13fccf100386b7d8a308eb055ef3c392cac359f5 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Tue, 23 Sep 2025 21:59:38 -0700 Subject: [PATCH 14/17] fix tests on windows --- dsc/tests/dsc.exit_code.tests.ps1 | 8 ++++++++ dsc/tests/dsc_metadata.tests.ps1 | 6 +++--- sshdconfig/tests/defaultshell.tests.ps1 | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/dsc/tests/dsc.exit_code.tests.ps1 b/dsc/tests/dsc.exit_code.tests.ps1 index 16c27905b..41a9ef2ea 100644 --- a/dsc/tests/dsc.exit_code.tests.ps1 +++ b/dsc/tests/dsc.exit_code.tests.ps1 @@ -2,6 +2,14 @@ # Licensed under the MIT License. Describe 'exit code tests' { + BeforeAll { + $env:DSC_TRACE_LEVEL = 'error' + } + + AfterAll { + $env:DSC_TRACE_LEVEL = $null + } + It 'non-zero exit code in manifest has corresponding message' { dsc resource get -r Test/ExitCode --input "{ exitCode: 8 }" 2> $TestDrive/tracing.txt "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Placeholder from manifest for exit code 8' diff --git a/dsc/tests/dsc_metadata.tests.ps1 b/dsc/tests/dsc_metadata.tests.ps1 index 095220f3c..cf29e2e31 100644 --- a/dsc/tests/dsc_metadata.tests.ps1 +++ b/dsc/tests/dsc_metadata.tests.ps1 @@ -23,7 +23,7 @@ Describe 'metadata tests' { '@ $out = dsc -l warn config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*WARN*Will not add '_metadata' to properties because resource schema does not support it*" $out.results.result.actualState.output | Should -BeExactly 'hello world' } @@ -144,7 +144,7 @@ Describe 'metadata tests' { $out.results.count | Should -Be 1 $out.results[0].metadata.validOne | Should -BeTrue $out.results[0].metadata.Microsoft.DSC | Should -BeNullOrEmpty - (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property 'Microsoft.DSC' which is ignored*" + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*WARN*Resource returned '_metadata' property 'Microsoft.DSC' which is ignored*" } It 'resource returning _restartRequired metadata is handled' { @@ -212,7 +212,7 @@ Describe 'metadata tests' { '@ $out = dsc -l warn config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log) | Should -BeLike "*WARN*Resource returned '_metadata' property '_restartRequired' which contains invalid value: ``[{`"invalid`":`"item`"}]*" + (Get-Content $TestDrive/error.log -Raw) | Should -BeLike "*WARN*Resource returned '_metadata' property '_restartRequired' which contains invalid value: ``[{`"invalid`":`"item`"}]*" $out.results[0].metadata._restartRequired | Should -BeNullOrEmpty } } diff --git a/sshdconfig/tests/defaultshell.tests.ps1 b/sshdconfig/tests/defaultshell.tests.ps1 index 913d46118..36fb574a4 100644 --- a/sshdconfig/tests/defaultshell.tests.ps1 +++ b/sshdconfig/tests/defaultshell.tests.ps1 @@ -16,6 +16,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated $RegistryPath = "HKLM:\SOFTWARE\OpenSSH" $ValueNames = @("DefaultShell", "DefaultShellCommandOption", "DefaultShellEscapeArguments") $CreatedOpenSSHKey = $false + $env:DSC_TRACE_LEVEL = 'error' # Create OpenSSH registry key if it doesn't exist if (-not (Test-Path $RegistryPath)) { @@ -51,6 +52,7 @@ Describe 'Default Shell Configuration Tests' -Skip:(!$IsWindows -or !$isElevated } } } + $env:DSC_TRACE_LEVEL = $null } AfterEach { From 9a0462bb14b57f4d919afd5d016319239f1b6ba1 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 26 Sep 2025 18:17:19 -0700 Subject: [PATCH 15/17] show trust level for listing extensions and resources --- dsc/locales/en-us.toml | 2 + dsc/src/subcommand.rs | 17 +++++ dsc/src/util.rs | 3 +- dsc/tests/dsc_security.tests.ps1 | 6 +- dsc_lib/locales/en-us.toml | 10 ++- dsc_lib/src/discovery/mod.rs | 6 +- dsc_lib/src/dscresources/command_resource.rs | 4 +- dsc_lib/src/security/authenticode_windows.rs | 26 ++++--- dsc_lib/src/security/mod.rs | 72 ++++++++++++++++---- 9 files changed, 109 insertions(+), 37 deletions(-) diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 5d109ca1d..bc175869b 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -131,6 +131,7 @@ tableHeader_functionCategory = "Category" tableHeader_minArgs = "MinArgs" tableHeader_maxArgs = "MaxArgs" tableHeader_argTypes = "ReturnTypes" +tableHeader_trust = "Trust" invalidFunctionFilter = "Invalid function filter" maxInt = "maxInt" invalidManifest = "Error in manifest for" @@ -157,3 +158,4 @@ dscConfigRootAlreadySet = "The current value of DSC_CONFIG_ROOT env var will be settingDscConfigRoot = "Setting DSC_CONFIG_ROOT env var as" stdinNotAllowedForBothParametersAndInput = "Cannot read from STDIN for both parameters and input." removingUtf8Bom = "Removing UTF-8 BOM from input" +failedToCheckFileSecurity = "Failed to check file security for '%{path}': %{error}" diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index bf559cf65..3a912f8f3 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -7,6 +7,7 @@ use crate::resource_command::{get_resource, self}; use crate::tablewriter::Table; use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR}; use dsc_lib::functions::FunctionArgKind; +use dsc_lib::security::{check_file_security, TrustLevel}; use dsc_lib::{ configure::{ config_doc::{ @@ -595,6 +596,7 @@ fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format t!("subcommand.tableHeader_type").to_string().as_ref(), t!("subcommand.tableHeader_version").to_string().as_ref(), t!("subcommand.tableHeader_capabilities").to_string().as_ref(), + t!("subcommand.tableHeader_trust").to_string().as_ref(), t!("subcommand.tableHeader_description").to_string().as_ref(), ]); if format.is_none() && io::stdout().is_terminal() { @@ -616,11 +618,17 @@ fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format } } + let trust_level = match check_file_security(Path::new(&extension.path)) { + Ok(trust_level) => trust_level, + Err(_err) => TrustLevel::Unknown, + }; + if write_table { table.add_row(vec![ extension.type_name, extension.version, capabilities, + trust_level.to_string(), extension.description.unwrap_or_default() ]); } @@ -661,6 +669,7 @@ fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String> t!("subcommand.tableHeader_minArgs").to_string().as_ref(), t!("subcommand.tableHeader_maxArgs").to_string().as_ref(), t!("subcommand.tableHeader_argTypes").to_string().as_ref(), + t!("subcommand.tableHeader_trust").to_string().as_ref(), t!("subcommand.tableHeader_description").to_string().as_ref(), ]); if output_format.is_none() && io::stdout().is_terminal() { @@ -744,6 +753,7 @@ fn list_functions(functions: &FunctionDispatcher, function_name: Option<&String> } } +#[allow(clippy::too_many_lines)] pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adapter_name: Option<&String>, description: Option<&String>, tags: Option<&Vec>, format: Option<&ListOutputFormat>, progress_format: ProgressFormat) { let mut write_table = false; let mut table = Table::new(&[ @@ -751,6 +761,7 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap t!("subcommand.tableHeader_kind").to_string().as_ref(), t!("subcommand.tableHeader_version").to_string().as_ref(), t!("subcommand.tableHeader_capabilities").to_string().as_ref(), + t!("subcommand.tableHeader_trust").to_string().as_ref(), t!("subcommand.tableHeader_adapter").to_string().as_ref(), t!("subcommand.tableHeader_description").to_string().as_ref(), ]); @@ -817,12 +828,18 @@ pub fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adap } } + let trust_level = match check_file_security(Path::new(&resource.path)) { + Ok(trust_level) => trust_level, + Err(_err) => TrustLevel::Unknown, + }; + if write_table { table.add_row(vec![ resource.type_name, format!("{:?}", resource.kind), resource.version, capabilities, + trust_level.to_string(), resource.require_adapter.unwrap_or_default(), resource.description.unwrap_or_default() ]); diff --git a/dsc/src/util.rs b/dsc/src/util.rs index b4a6c0d2b..2c9d09119 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -463,7 +463,8 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_ } } else { if let Err(err) = check_file_security(Path::new(path)) { - warn!("{err}"); + error!("{}", t!("util.failedToCheckFileSecurity", path = path, error = err)); + exit(EXIT_INVALID_INPUT); } // see if an extension should handle this file diff --git a/dsc/tests/dsc_security.tests.ps1 b/dsc/tests/dsc_security.tests.ps1 index d1c2ccfa2..371c742d7 100644 --- a/dsc/tests/dsc_security.tests.ps1 +++ b/dsc/tests/dsc_security.tests.ps1 @@ -5,18 +5,18 @@ Describe 'Tests for security features' { It 'Unsigned config file gives warning' -Skip:(!$IsWindows) { $null = dsc config get -f $PSScriptRoot/../examples/osinfo_parameters.dsc.yaml 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN File '.*?\\osinfo_parameters.dsc.yaml' is not signed.*?" } It 'Unsigned resource manifest gives warning' -Skip:(!$IsWindows) { $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo.dsc.resource.json' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN File '.*?\\osinfo.dsc.resource.json' is not signed.*?" } It 'Unsigned resource executable gives warning' -Skip:(!$IsWindows) { $null = dsc resource get -r Microsoft/OSInfo 2>$TestDrive/error.log $LASTEXITCODE | Should -Be 0 - (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN Authenticode: File '.*?\\osinfo.exe' is not signed.*?" + (Get-Content $TestDrive/error.log -Raw) | Should -Match "WARN File '.*?\\osinfo.exe' is not signed.*?" } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index b124ceb95..2c7d9cffe 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -591,11 +591,19 @@ invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{ failedToSerialize = "Failed to serialize progress JSON: %{json}" [security.authenticode] +trustLevelTrusted = "Trusted" +trustLevelExplicitlyDistrusted = "Distrusted" +trustLevelUnsigned = "Unsigned" +trustLevelUntrusted = "Untrusted" +trustLevelNotMeetSecuritySettings = "NotMeetSecuritySettings" +trustLevelCannotBeVerified = "CannotVerify" +trustLevelUnknown = "Unknown" fileNotSigned = "File '%{file}' is not signed" signatureExplicitlyDistrusted = "The signature for file '%{file}' is explicitly distrusted" signatureNotTrusted = "The signature for file '%{file}' is not trusted" signatureDoesNotMeetSecuritySettings = "The signature for file '%{file}' does not meet the security settings" -signatureCouldNotBeVerified = "The signature for file '%{file}' could not be verified. HRESULT: 0x%{hresult}" +signatureCouldNotBeVerified = "The signature for file '%{file}' could not be verified" +trustLevelIsUnknown = "The trust level for file '%{file}' is unknown" [util] foundSetting = "Found setting '%{name}' in %{path}" diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index 164663f1e..d3d1d82d4 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -12,7 +12,7 @@ use core::result::Result::Ok; use semver::{Version, VersionReq}; use std::{collections::BTreeMap, path::Path}; use command_discovery::{CommandDiscovery, ImportedManifest}; -use tracing::{error, warn}; +use tracing::error; #[derive(Clone)] pub struct Discovery { @@ -123,8 +123,8 @@ impl Discovery { }; if let Some(found_resource) = &resource { - if let Err(err) = check_file_security(Path::new(&found_resource.path)) { - warn!("{err}"); + if check_file_security(Path::new(&found_resource.path)).is_err() { + return None; } } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 17b685da1..1f9be3732 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -598,9 +598,7 @@ async fn run_process_async(executable: &str, args: Option>, input: O const INITIAL_BUFFER_CAPACITY: usize = 1024*1024; let exe = which(executable)?; - if let Err(err) = check_file_security(&exe) { - warn!("{err}"); - } + check_file_security(&exe)?; let mut command = Command::new(executable); if input.is_some() { diff --git a/dsc_lib/src/security/authenticode_windows.rs b/dsc_lib/src/security/authenticode_windows.rs index bfe16b323..39e48eea3 100644 --- a/dsc_lib/src/security/authenticode_windows.rs +++ b/dsc_lib/src/security/authenticode_windows.rs @@ -2,8 +2,7 @@ // Licensed under the MIT License. use crate::dscerror::DscError; -use crate::security::{add_file_as_checked, is_file_checked}; -use rust_i18n::t; +use crate::security::{get_file_trust_level, is_file_checked, TrustLevel}; use std::{ ffi::OsStr, mem::size_of, @@ -45,9 +44,9 @@ use windows_result::HRESULT; /// * `Ok(())` if the file is signed and the signature is valid. /// * `Err(DscError)` if the file is not signed or the signature is invalid /// -pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { +pub fn check_authenticode(file_path: &Path) -> Result { if is_file_checked(file_path) { - return Ok(()); + return Ok(get_file_trust_level(file_path)); } let wintrust_file_info = WINTRUST_FILE_INFO { @@ -97,17 +96,16 @@ pub fn check_authenticode(file_path: &Path) -> Result<(), DscError> { (&raw const wintrust_data).cast_mut(), ) }; - add_file_as_checked(file_path); - - if hresult.is_ok() { - Ok(()) + let trust_level = if hresult.is_ok() { + TrustLevel::Trusted } else { match hresult { - TRUST_E_NOSIGNATURE => Err(DscError::AuthenticodeError(t!("security.authenticode.fileNotSigned", file = file_path.display()).to_string())), - TRUST_E_EXPLICIT_DISTRUST => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display()).to_string())), - TRUST_E_SUBJECT_NOT_TRUSTED => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureNotTrusted", file = file_path.display()).to_string())), - CRYPT_E_SECURITY_SETTINGS => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display()).to_string())), - _ => Err(DscError::AuthenticodeError(t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display(), hresult = hresult.0 : {:x}).to_string())), + TRUST_E_NOSIGNATURE => TrustLevel::Unsigned, + TRUST_E_EXPLICIT_DISTRUST => TrustLevel::ExplicitlyDistrusted, + TRUST_E_SUBJECT_NOT_TRUSTED => TrustLevel::Untrusted, + CRYPT_E_SECURITY_SETTINGS => TrustLevel::NotMeetSecuritySettings, + _ => TrustLevel::CannotBeVerified, } - } + }; + Ok(trust_level) } diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index ac0c1ae1f..0799e61d9 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -3,32 +3,69 @@ #[cfg(windows)] use authenticode_windows::check_authenticode; -use std::path::Path; +use rust_i18n::t; +use std::{ + collections::HashMap, + fmt::Display, + path::Path, +}; #[cfg(windows)] use std::sync::LazyLock; +use tracing::warn; use crate::dscerror::DscError; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TrustLevel { + Trusted, + ExplicitlyDistrusted, + Unsigned, + Untrusted, + NotMeetSecuritySettings, + CannotBeVerified, + Unknown, +} + +impl Display for TrustLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + TrustLevel::Trusted => t!("security.authenticode.trustLevelTrusted"), + TrustLevel::ExplicitlyDistrusted => t!("security.authenticode.trustLevelExplicitlyDistrusted"), + TrustLevel::Unsigned => t!("security.authenticode.trustLevelUnsigned"), + TrustLevel::Untrusted => t!("security.authenticode.trustLevelUntrusted"), + TrustLevel::NotMeetSecuritySettings => t!("security.authenticode.trustLevelNotMeetSecuritySettings"), + TrustLevel::CannotBeVerified => t!("security.authenticode.trustLevelCannotBeVerified"), + TrustLevel::Unknown => t!("security.authenticode.trustLevelUnknown"), + }; + write!(f, "{s}") + } +} + #[cfg(windows)] mod authenticode_windows; #[cfg(windows)] -static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(vec![])); +static CHECKED_FILES: LazyLock>> = LazyLock::new(|| std::sync::Mutex::new(HashMap::new())); #[cfg(windows)] -fn add_file_as_checked(file_path: &Path) { +fn add_file_as_checked(file_path: &Path, trust_level: TrustLevel) { let file_str = file_path.to_string_lossy().to_string(); let mut checked_files = CHECKED_FILES.lock().unwrap(); - if !checked_files.contains(&file_str) { - checked_files.push(file_str); - } + checked_files.entry(file_str).or_insert(trust_level); } #[cfg(windows)] fn is_file_checked(file_path: &Path) -> bool { let file_str = file_path.to_string_lossy().to_string(); let checked_files = CHECKED_FILES.lock().unwrap(); - checked_files.contains(&file_str) + checked_files.contains_key(&file_str) +} + +#[cfg(windows)] +fn get_file_trust_level(file_path: &Path) -> TrustLevel { + let file_str = file_path.to_string_lossy().to_string(); + let checked_files = CHECKED_FILES.lock().unwrap(); + checked_files.get(&file_str).copied().unwrap_or(TrustLevel::Unknown) } /// Check the security of a file. @@ -37,15 +74,26 @@ fn is_file_checked(file_path: &Path) -> bool { /// * `file_path` - The path to the file to check. /// /// # Returns -/// * `Ok(())` if the file passes the security checks. -/// * `Err(DscError)` if the file fails the security checks. +/// * `Ok(TrustLevel)` if the file was checked successfully, with its trust level. /// /// # Errors /// This function will return an error if the Authenticode check fails on Windows. #[cfg(windows)] -pub fn check_file_security(file_path: &Path) -> Result<(), DscError> { - check_authenticode(file_path)?; - Ok(()) +pub fn check_file_security(file_path: &Path) -> Result { + let trust_level = check_authenticode(file_path)?; + if !is_file_checked(file_path) { + add_file_as_checked(file_path, trust_level); + match trust_level { + TrustLevel::Trusted => {}, + TrustLevel::ExplicitlyDistrusted => warn!("{}", t!("security.authenticode.signatureExplicitlyDistrusted", file = file_path.display())), + TrustLevel::Unsigned => warn!("{}", t!("security.authenticode.fileNotSigned", file = file_path.display())), + TrustLevel::Untrusted => warn!("{}", t!("security.authenticode.signatureNotTrusted", file = file_path.display())), + TrustLevel::NotMeetSecuritySettings => warn!("{}", t!("security.authenticode.signatureDoesNotMeetSecuritySettings", file = file_path.display())), + TrustLevel::CannotBeVerified => warn!("{}", t!("security.authenticode.signatureCouldNotBeVerified", file = file_path.display())), + TrustLevel::Unknown => warn!("{}", t!("security.authenticode.trustLevelIsUnknown", file = file_path.display())), + } + } + Ok(trust_level) } /// On non-Windows platforms, this function is a no-op. From a5392a19c72ce6a0a2e4aa224a4b42e3b08709d9 Mon Sep 17 00:00:00 2001 From: "Steve Lee (POWERSHELL HE/HIM) (from Dev Box)" Date: Fri, 26 Sep 2025 18:24:28 -0700 Subject: [PATCH 16/17] fix build on non-windows --- dsc_lib/src/security/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index 0799e61d9..daa196a76 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -4,13 +4,15 @@ #[cfg(windows)] use authenticode_windows::check_authenticode; use rust_i18n::t; +#[cfg(windows)] +use std::collections::HashMap; use std::{ - collections::HashMap, fmt::Display, path::Path, }; #[cfg(windows)] use std::sync::LazyLock; +#[cfg(windows)] use tracing::warn; use crate::dscerror::DscError; From 87e3db44d5a01f1461fb05980c81be41e5a4a19e Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Fri, 26 Sep 2025 18:34:09 -0700 Subject: [PATCH 17/17] fix non-windows build --- dsc_lib/src/security/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsc_lib/src/security/mod.rs b/dsc_lib/src/security/mod.rs index daa196a76..bbf9b3077 100644 --- a/dsc_lib/src/security/mod.rs +++ b/dsc_lib/src/security/mod.rs @@ -109,6 +109,6 @@ pub fn check_file_security(file_path: &Path) -> Result { /// # Errors /// This function does not return any errors on non-Windows platforms. #[cfg(not(windows))] -pub fn check_file_security(_file_path: &Path) -> Result<(), DscError> { - Ok(()) +pub fn check_file_security(_file_path: &Path) -> Result { + Ok(TrustLevel::Unknown) }