From 2dc1df00274c9f17112fae5caf0b11cd9702e4ff Mon Sep 17 00:00:00 2001 From: Steve Lee Date: Tue, 21 Mar 2023 16:45:12 -0700 Subject: [PATCH] add schema validation --- dsc/src/main.rs | 11 ++++--- dsc/tests/dsc_get.tests.ps1 | 11 +++++++ dsc_lib/Cargo.toml | 1 + dsc_lib/src/dscerror.rs | 13 +++++---- dsc_lib/src/dscresources/command_resource.rs | 29 +++++++++++++++++++ dsc_lib/src/dscresources/invoke_result.rs | 3 ++ dsc_lib/src/dscresources/resource_manifest.rs | 1 + registry/Cargo.toml | 4 +-- registry/src/config.rs | 2 +- 9 files changed, 63 insertions(+), 12 deletions(-) diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 6d2ca45e..e012999d 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -376,13 +376,16 @@ fn get_input(input: &Option, stdin: &Option) -> String { #[cfg(debug_assertions)] fn check_debug() { - if env::var("DEBUG_CONFIG").is_ok() { - eprintln!("attach debugger to pid {} and press any key to continue", std::process::id()); + if env::var("DEBUG_DSC").is_ok() { + eprintln!("attach debugger to pid {} and press a key to continue", std::process::id()); loop { let event = event::read().unwrap(); match event { - event::Event::Key(_key) => { - break; + event::Event::Key(key) => { + // workaround bug in 0.26+ https://github.com/crossterm-rs/crossterm/issues/752#issuecomment-1414909095 + if key.kind == event::KeyEventKind::Press { + break; + } } _ => { eprintln!("Unexpected event: {:?}", event); diff --git a/dsc/tests/dsc_get.tests.ps1 b/dsc/tests/dsc_get.tests.ps1 index 0e1809d6..c1ddc1dd 100644 --- a/dsc/tests/dsc_get.tests.ps1 +++ b/dsc/tests/dsc_get.tests.ps1 @@ -35,4 +35,15 @@ Describe 'config get tests' { $output.actual_state.valueName | Should -BeExactly 'ProductName' $output.actual_state.valueData.String | Should -Match 'Windows .*' } + + It 'invalid input is validated against schema' -Skip:(!$IsWindows) { + $json = @' + { + "keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion", + "Name": "ProductName" + } +'@ + $json | dsc resource get -r registry + $LASTEXITCODE | Should -Be 2 + } } diff --git a/dsc_lib/Cargo.toml b/dsc_lib/Cargo.toml index b4833e89..26a5fd52 100644 --- a/dsc_lib/Cargo.toml +++ b/dsc_lib/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] derive_builder ="0.12" +jsonschema = "0.17" regex = "1.7" reqwest = { version = "0.11", features = ["blocking"] } schemars = { version = "0.8.12" } diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index ee706164..d3a23d27 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -3,12 +3,15 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum DscError { - #[error("HTTP status: {0}")] - HttpStatus(StatusCode), + #[error("Command: [{0}] {1}")] + Command(i32, String), #[error("HTTP: {0}")] Http(#[from] reqwest::Error), + #[error("HTTP status: {0}")] + HttpStatus(StatusCode), + #[error("IO: {0}")] Io(#[from] std::io::Error), @@ -18,9 +21,6 @@ pub enum DscError { #[error("Manifest: {0}\nJSON: {1}")] Manifest(String, serde_json::Error), - #[error("Command: [{0}] {1}")] - Command(i32, String), - #[error("Missing manifest: {0}")] MissingManifest(String), @@ -33,6 +33,9 @@ pub enum DscError { #[error("Operation: {0}")] Operation(String), + #[error("Schema: {0}")] + Schema(String), + #[error("Unknown: {code:?} {message:?}")] Unknown { code: i32, diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 8ea47aa9..01b53a00 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -1,3 +1,4 @@ +use jsonschema::JSONSchema; use serde_json::Value; use std::{process::Command, io::{Write, Read}, process::Stdio}; @@ -7,6 +8,8 @@ use super::{resource_manifest::{ResourceManifest, ReturnKind, SchemaKind}, invok pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; pub fn invoke_get(resource: &ResourceManifest, filter: &str) -> Result { + verify_json(resource, filter)?; + let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone().unwrap_or_default(), Some(filter))?; if exit_code != 0 { return Err(DscError::Command(exit_code, stderr.to_string())); @@ -19,6 +22,8 @@ pub fn invoke_get(resource: &ResourceManifest, filter: &str) -> Result Result { + verify_json(resource, desired)?; + // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !resource.set.pre_test.unwrap_or_default() { let test_result = invoke_test(resource, desired)?; @@ -83,6 +88,8 @@ pub fn invoke_set(resource: &ResourceManifest, desired: &str) -> Result Result { + verify_json(resource, expected)?; + let (exit_code, stdout, stderr) = invoke_command(&resource.test.executable, resource.test.args.clone().unwrap_or_default(), Some(expected))?; if exit_code != 0 { return Err(DscError::Command(exit_code, stderr.to_string())); @@ -181,6 +188,28 @@ fn invoke_command(executable: &str, args: Vec, input: Option<&str>) -> R Ok((exit_code, stdout, stderr)) } +fn verify_json(resource: &ResourceManifest, json: &str) -> Result<(), DscError> { + let schema = invoke_schema(resource)?; + let schema: Value = serde_json::from_str(&schema)?; + let compiled_schema = match JSONSchema::compile(&schema) { + Ok(schema) => schema, + Err(e) => { + return Err(DscError::Schema(e.to_string())); + }, + }; + let json: Value = serde_json::from_str(json)?; + match compiled_schema.validate(&json) { + Ok(_) => return Ok(()), + Err(err) => { + let mut error = String::new(); + for e in err { + error.push_str(&format!("{} ", e)); + } + return Err(DscError::Schema(error)); + }, + }; +} + fn get_diff(expected: &Value, actual: &Value) -> Vec { let mut diff_properties: Vec = Vec::new(); if expected.is_null() { diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index c8c5d515..723699de 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct GetResult { pub actual_state: Value, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct SetResult { pub before_state: Value, pub after_state: Value, @@ -15,6 +17,7 @@ pub struct SetResult { } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct TestResult { pub expected_state: Value, pub actual_state: Value, diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index 30a60f7c..e44430b9 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] pub struct ResourceManifest { #[serde(rename = "manifestVersion")] pub manifest_version: String, diff --git a/registry/Cargo.toml b/registry/Cargo.toml index e268257b..210bff96 100644 --- a/registry/Cargo.toml +++ b/registry/Cargo.toml @@ -15,8 +15,8 @@ lto = true [dependencies] atty = { version = "0.2" } -clap = { version = "3.2", features = ["derive"] } -crossterm = { version = "0.24.0" } +clap = { version = "4.1", features = ["derive"] } +crossterm = { version = "0.26" } ntreg = { path = "../ntreg" } ntstatuserror = { path = "../ntstatuserror" } schemars = { version = "0.8.12" } diff --git a/registry/src/config.rs b/registry/src/config.rs index 06b280e7..e72007f2 100644 --- a/registry/src/config.rs +++ b/registry/src/config.rs @@ -18,7 +18,7 @@ pub enum RegistryValueData { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] -#[serde(rename = "Registry")] +#[serde(rename = "Registry", deny_unknown_fields)] pub struct RegistryConfig { #[serde(rename = "$id", skip_serializing_if = "Option::is_none")] pub id: Option,