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/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 57d85cdc0..d8be4514d 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 f2504b8a0..2c9d09119 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,11 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_ } } } else { + if let Err(err) = check_file_security(Path::new(path)) { + error!("{}", t!("util.failedToCheckFileSecurity", path = path, error = err)); + exit(EXIT_INVALID_INPUT); + } + // 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.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.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_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..75fee3f82 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' { @@ -94,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_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..cf29e2e31 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 @@ -13,9 +21,9 @@ 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*" + (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' } @@ -131,12 +139,12 @@ 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 $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' { @@ -202,9 +210,9 @@ 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`"}]*" + (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/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 new file mode 100644 index 000000000..371c742d7 --- /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 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 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 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/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..e8e6d650a 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -50,6 +50,16 @@ 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", + "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/locales/en-us.toml b/dsc_lib/locales/en-us.toml index ada29a34c..2c7d9cffe 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -590,6 +590,21 @@ invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{ [progress] 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" +trustLevelIsUnknown = "The trust level for file '%{file}' is unknown" + [util] foundSetting = "Found setting '%{name}' in %{path}" notFoundSetting = "Setting '%{name}' not found in %{path}" diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index f96f94d09..d3d1d82d4 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -6,10 +6,11 @@ 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; @@ -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 check_file_security(Path::new(&found_resource.path)).is_err() { + return None; + } } + + 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..1f9be3732 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,9 @@ 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)?; + check_file_security(&exe)?; + 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_windows.rs b/dsc_lib/src/security/authenticode_windows.rs new file mode 100644 index 000000000..39e48eea3 --- /dev/null +++ b/dsc_lib/src/security/authenticode_windows.rs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::dscerror::DscError; +use crate::security::{get_file_trust_level, is_file_checked, TrustLevel}; +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 { + if is_file_checked(file_path) { + return Ok(get_file_trust_level(file_path)); + } + + 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(), + ) }; + + let trust_level = if hresult.is_ok() { + TrustLevel::Trusted + } else { + match hresult { + 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 new file mode 100644 index 000000000..bbf9b3077 --- /dev/null +++ b/dsc_lib/src/security/mod.rs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[cfg(windows)] +use authenticode_windows::check_authenticode; +use rust_i18n::t; +#[cfg(windows)] +use std::collections::HashMap; +use std::{ + fmt::Display, + path::Path, +}; +#[cfg(windows)] +use std::sync::LazyLock; +#[cfg(windows)] +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(HashMap::new())); + +#[cfg(windows)] +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(); + 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_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. +/// +/// # Arguments +/// * `file_path` - The path to the file to check. +/// +/// # Returns +/// * `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 { + 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. +/// +/// # 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 { + Ok(TrustLevel::Unknown) +} 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/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 { 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" diff --git a/tools/test_group_resource/tests/provider.tests.ps1 b/tools/test_group_resource/tests/provider.tests.ps1 index 140dd1fb5..567f2a68e 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' { @@ -56,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'*" } 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 } }