diff --git a/Cargo.lock b/Cargo.lock index 4f7c51e..e1f54f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1397,6 +1397,8 @@ dependencies = [ "const_format", "icann-rdap-common", "idna", + "jsonpath-rust", + "jsonpath_lib", "lazy_static", "pct-str", "regex", @@ -1548,6 +1550,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonpath-rust" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0268078319393f8430e850ee9d4706aeced256d34cf104d216bb496777137162" +dependencies = [ + "lazy_static", + "once_cell", + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror", +] + +[[package]] +name = "jsonpath_lib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f" +dependencies = [ + "log", + "serde", + "serde_json", +] + [[package]] name = "lazy-regex" version = "3.1.0" @@ -1994,6 +2022,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.41", +] + +[[package]] +name = "pest_meta" +version = "2.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -2500,6 +2573,7 @@ version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -3302,6 +3376,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-bidi" version = "0.3.14" diff --git a/icann-rdap-cli/src/query.rs b/icann-rdap-cli/src/query.rs index 0f7caf7..13f512a 100644 --- a/icann-rdap-cli/src/query.rs +++ b/icann-rdap-cli/src/query.rs @@ -6,7 +6,7 @@ use tracing::error; use tracing::info; use icann_rdap_client::{ - md::{MdOptions, MdParams, ToMd}, + md::{redacted::replace_redacted_items, MdOptions, MdParams, ToMd}, query::{qtype::QueryType, request::ResponseData}, request::{RequestData, RequestResponse, RequestResponses, SourceType}, }; @@ -84,7 +84,19 @@ async fn do_domain_query<'a, W: std::io::Write>( source_host: &source_host, source_type: SourceType::DomainRegistry, }; - transactions = do_output(processing_params, &req_data, &response, write, transactions)?; + let replaced_rdap = replace_redacted_items(response.rdap.clone()); + let replaced_data = ResponseData { + rdap: replaced_rdap, + // copy other fields from `response` + ..response.clone() + }; + transactions = do_output( + processing_params, + &req_data, + &replaced_data, + write, + transactions, + )?; let regr_source_host; let regr_req_data: RequestData; if let Some(url) = get_related_link(&response.rdap).first() { @@ -135,7 +147,19 @@ async fn do_inr_query<'a, W: std::io::Write>( source_host: &source_host, source_type: SourceType::RegionalInternetRegistry, }; - transactions = do_output(processing_params, &req_data, &response, write, transactions)?; + let replaced_rdap = replace_redacted_items(response.rdap.clone()); + let replaced_data = ResponseData { + rdap: replaced_rdap, + // copy other fields from `response` + ..response.clone() + }; + transactions = do_output( + processing_params, + &req_data, + &replaced_data, + write, + transactions, + )?; do_final_output(processing_params, write, transactions)?; } Err(error) => return Err(error), @@ -169,7 +193,19 @@ async fn do_basic_query<'a, W: std::io::Write>( source_type: SourceType::UncategorizedRegistry, } }; - transactions = do_output(processing_params, &req_data, &response, write, transactions)?; + let replaced_rdap = replace_redacted_items(response.rdap.clone()); + let replaced_data = ResponseData { + rdap: replaced_rdap, + // copy other fields from `response` + ..response.clone() + }; + transactions = do_output( + processing_params, + &req_data, + &replaced_data, + write, + transactions, + )?; do_final_output(processing_params, write, transactions)?; } Err(error) => return Err(error), diff --git a/icann-rdap-client/Cargo.toml b/icann-rdap-client/Cargo.toml index bd15b61..f86027a 100644 --- a/icann-rdap-client/Cargo.toml +++ b/icann-rdap-client/Cargo.toml @@ -27,6 +27,9 @@ strum.workspace = true strum_macros.workspace = true thiserror.workspace = true +jsonpath-rust = "=0.5.0" +jsonpath_lib = "0.3.0" + [dev-dependencies] # fixture testings diff --git a/icann-rdap-client/src/md/redacted.rs b/icann-rdap-client/src/md/redacted.rs index 68d3561..8751946 100644 --- a/icann-rdap-client/src/md/redacted.rs +++ b/icann-rdap-client/src/md/redacted.rs @@ -1,6 +1,13 @@ +use std::str::FromStr; + use icann_rdap_common::response::redacted::Redacted; +use jsonpath::replace_with; +use jsonpath_lib as jsonpath; +use jsonpath_rust::{JsonPathFinder, JsonPathInst}; +use serde_json::{json, Value}; use super::{string::StringUtil, table::MultiPartTable, MdOptions, MdParams, ToMd}; +use icann_rdap_common::response::RdapResponse; impl ToMd for &[Redacted] { fn to_md(&self, params: MdParams) -> String { @@ -61,3 +68,501 @@ impl ToMd for &[Redacted] { md } } + +// These are the different types of results that we can get from the JSON path checks +#[derive(Debug, PartialEq, Clone)] +pub enum ResultType { + StringNoValue, // (*) what we found in the value paths array was a string but has no value (yes, this is a little weird, but does exist) `Redaction by Empty Value` + EmptyString, // (*) what we found in the value paths array was a string but it is an empty string `Redaction by Empty Value` + PartialString, // (*) what we found in the value paths array was a string and it does have a value `Redaction by Partial Value` and/or `Redaction by Replacement Value` + Array, // what we found in the value paths array was _another_ array (have never found this w/ redactions done correctly) + Object, // what we found in the value paths array was an object (have never found this w/ redactions done correctly) + Removed, // (*) paths array is empty, finder.find_as_path() found nothing `Redaction by Removal` + FoundNull, // value in paths array is null (have never found this w/ redactions done correctly) + FoundNothing, // fall through, value in paths array is not anything else (have never found this w/ redactions done correctly) + FoundUnknown, // what we found was not a JSON::Value::string (have never found this w/ redactions done correctly) + FoundPathReturnedBadValue, // what finder.find_as_path() returned was not a Value::Array (have never found this, could possibly be an error) +} + +#[derive(Debug, Clone)] +pub struct RedactedObject { + pub name: Value, // Get the description's name or type + pub path_index_count: i32, // how many paths does the json resolve to? + pub pre_path: Option, // the prePath + pub post_path: Option, // the postPath + pub original_path: Option, // the original path that was put into the redaction + pub final_path: Vec>, // a vector of the paths where we put a partialValue or emptyValue + pub do_final_path_subsitution: bool, // if we are modifying anything or not + pub path_lang: Value, // the path_lang they put in, these may be used in the future + pub replacement_path: Option, + pub method: Value, // the method they are using + pub reason: Value, // the reason + pub result_type: Vec>, // a vec of our own internal Results we found + pub redaction_type: Option, // +} + +// This isn't just based on the string type that is in the redaction method, but also based on the result type above +#[derive(Debug, PartialEq, Clone)] +pub enum RedactionType { + EmptyValue, + PartialValue, + ReplacementValue, + Removal, + Unknown, +} + +// this is our public entry point +pub fn replace_redacted_items(orignal_response: RdapResponse) -> RdapResponse { + // convert the RdapResponse to a string + let rdap_json = serde_json::to_string(&orignal_response).unwrap(); + // convert the string to a JSON Value + let mut rdap_json_response: Value = serde_json::from_str(&rdap_json).unwrap(); + // Initialize the final response with the original response + let mut response = orignal_response; + // pull the redacted array out of the JSON + let redacted_array_option = rdap_json_response["redacted"].as_array().cloned(); + + // if there are any redactions we need to do some modifications + if let Some(ref redacted_array) = redacted_array_option { + parse_redacted_json(&mut rdap_json_response, Some(redacted_array)); + // convert the Value back to a RdapResponse + response = serde_json::from_value(rdap_json_response).unwrap(); + } // END if there are redactions + + // send the response back so we can display it to the client + response +} + +fn parse_redacted_json( + rdap_json_response: &mut serde_json::Value, + redacted_array_option: Option<&Vec>, +) { + if let Some(redacted_array) = redacted_array_option { + let redactions = parse_redacted_array(rdap_json_response, redacted_array); + // Loop through the RedactedObjects + for redacted_object in redactions { + // If we have determined we are doing some kind of substitution + if redacted_object.do_final_path_subsitution && !redacted_object.final_path.is_empty() { + let path_count = redacted_object.path_index_count as usize; + for path_index_count in 0..path_count { + let final_path_option = &redacted_object.final_path[path_index_count]; + if let Some(final_path) = final_path_option { + // This is a replacement and we SHOULD NOT be doing this until it is sorted out. + // For experimental reasons though, we shall continue. + if let Some(redaction_type) = &redacted_object.redaction_type { + if *redaction_type == RedactionType::ReplacementValue { + // let replacement_path_str; + // if let Some(replacement_path) = + // redacted_object.replacement_path.as_ref() + // { + // replacement_path_str = + // convert_to_json_pointer_path(replacement_path); + // } else { + // continue; + // } + // let final_replacement_value = match rdap_json_response + // .pointer(&replacement_path_str) + // { + // Some(value) => { + // value + // } + // None => { + // continue; + // } + // }; + // // Unwrap final_path and replacement_path to get a String and then get a reference to the String to get a &str + // let final_path = redacted_object + // // .final_path + // .final_path[path_index_count] + // .as_ref() + // .expect("final_path is None"); + + // // With the redaction I am saying that I am replacing the value at the prePath with the value from the replacementPath. + // // So, in essence, it is a copy. replacementPath = source, prePath = target. + // match replace_with( + // rdap_json_response.clone(), + // final_path, + // &mut |_| Some(json!(final_replacement_value)), + // ) { + // Ok(new_v) => { + // *rdap_json_response = new_v; + // } + // Err(e) => { + // dbg!(e); + // } + //} // end match replace_with + } else if *redaction_type == RedactionType::EmptyValue + || *redaction_type == RedactionType::PartialValue + { + // convert the final_path to a json pointer path + let final_path_str = convert_to_json_pointer_path(final_path); + + // grab the value at the end point of the JSON path + let final_value = match rdap_json_response.pointer(&final_path_str) + { + Some(value) => value.clone(), + None => { + continue; + } + }; + + // actually do the replace_with + let replaced_json = replace_with( + rdap_json_response.clone(), + final_path, + &mut |x| { + // STRING ONLY! This is the only spot where we are ACTUALLY replacing or updating something + if x.is_string() { + match x.as_str() { + Some("") => Some(json!("*REDACTED*")), + Some(s) => Some(json!(format!("*{}*", s))), + _ => Some(json!("*REDACTED*")), + } + } else { + // it isn't a string, we aren't going to do anything with it + Some(final_value.clone()) // copy the found value back to it + } + }, + ); + // Now we check if we did something + match replaced_json { + Ok(new_v) => { + *rdap_json_response = new_v; // we replaced something so now we need to update the response + } + _ => { + // Do nothing but we need to investigate why this is happening + } + } // end match replace_with + } // end if doing partialValue or emptyValue + } // end if redaction_type + } // end if final_path_option + } // end for each path_index_count + } // end if final_path + } // end loop over redactions + } // end if there is a redacted array +} + +// This cleans it up into a json pointer which is what we need to use to get the value +fn convert_to_json_pointer_path(path: &str) -> String { + let pointer_path = path + .trim_start_matches('$') + .replace('.', "/") + .replace("['", "/") + .replace("']", "") + .replace('[', "/") + .replace(']', "") + .replace("//", "/"); + pointer_path +} + +fn parse_redacted_array( + rdap_json_response: &Value, + redacted_array: &Vec, +) -> Vec { + let mut list_of_redactions: Vec = Vec::new(); + + for item in redacted_array { + let item_map = item.as_object().unwrap(); + let pre_path = item_map + .get("prePath") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let post_path = item_map + .get("postPath") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + // this is the original_path given to us + let original_path = pre_path.clone().or(post_path.clone()); + let mut redacted_object = RedactedObject { + name: Value::String(String::from("")), // Set to empty string initially + path_index_count: 0, // Set to 0 initially + pre_path, + post_path, + original_path, + final_path: Vec::new(), // final path we are doing something with + do_final_path_subsitution: false, // flag whether we ACTUALLY doing something or not + path_lang: item_map + .get("pathLang") + .unwrap_or(&Value::String(String::from(""))) + .clone(), + replacement_path: item_map + .get("replacementPath") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()), + method: item_map + .get("method") + .unwrap_or(&Value::String(String::from(""))) + .clone(), + reason: Value::String(String::from("")), // Set to empty string initially + result_type: Vec::new(), // Set to an empty Vec> initially + redaction_type: None, // Set to None initially + }; + + // Check if the "name" field is an object + if let Some(Value::Object(name_map)) = item_map.get("name") { + // If the "name" field contains a "description" or "type" field, use it to replace the "name" field in the RedactedObject + if let Some(name_value) = name_map.get("description").or_else(|| name_map.get("type")) { + redacted_object.name = name_value.clone(); + } + } + + // Check if the "reason" field is an object + if let Some(Value::Object(reason_map)) = item_map.get("reason") { + // If the "reason" field contains a "description" or "type" field, use it to replace the "reason" field in the RedactedObject + if let Some(reason_value) = reason_map + .get("description") + .or_else(|| reason_map.get("type")) + { + redacted_object.reason = reason_value.clone(); + } + } + + // this has to happen here, before everything else + redacted_object = + set_result_type_from_json_path(rdap_json_response.clone(), &mut redacted_object); + + // check the method and result_type to determine the redaction_type + if let Some(method) = redacted_object.method.as_str() { + // we don't just assume you are what you say you are... + match method { + "emptyValue" => { + if !redacted_object.result_type.is_empty() { + // I have relaxed the rules around this one, so we can have partialValue as well counts, so if someone has inadvertently added a partialValue to an emptyValue, it will still be redacted + if redacted_object.result_type.iter().all(|result_type| { + matches!( + result_type, + Some(ResultType::StringNoValue) + | Some(ResultType::EmptyString) + | Some(ResultType::PartialString) + ) + }) { + redacted_object.redaction_type = Some(RedactionType::EmptyValue); + } else { + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } else { + // the result_type is empty, so we don't know what it is + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } + "partialValue" => { + if !redacted_object.result_type.is_empty() { + if redacted_object.result_type.iter().all(|result_type| { + // matches!(result_type, Some(ResultType::PartialString)) + matches!( + result_type, + Some(ResultType::StringNoValue) + | Some(ResultType::EmptyString) + | Some(ResultType::PartialString) + ) + }) { + redacted_object.redaction_type = Some(RedactionType::PartialValue); + } else { + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } else { + // the result_type is empty, so we don't know what it is + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } + "replacementValue" => { + if !redacted_object.result_type.is_empty() { + if redacted_object.result_type.iter().all(|result_type| { + matches!(result_type, Some(ResultType::PartialString)) + }) { + if redacted_object.replacement_path.is_some() + && !redacted_object + .replacement_path + .as_ref() + .unwrap() + .is_empty() + && (redacted_object.pre_path.is_some() + && !redacted_object.pre_path.as_ref().unwrap().is_empty() + || redacted_object.post_path.is_some() + && !redacted_object.post_path.as_ref().unwrap().is_empty()) + { + redacted_object.redaction_type = + Some(RedactionType::ReplacementValue); + } else if redacted_object.replacement_path.is_none() + && (redacted_object.pre_path.is_some() + && !redacted_object.pre_path.as_ref().unwrap().is_empty() + || redacted_object.post_path.is_some() + && !redacted_object.post_path.as_ref().unwrap().is_empty()) + { + // this logic is really a partial value + redacted_object.redaction_type = Some(RedactionType::PartialValue); + } else { + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } else { + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } else { + // the result_type is empty, so we don't know what it is + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } + "removal" => { + if !redacted_object.result_type.is_empty() { + if redacted_object + .result_type + .iter() + .all(|result_type| matches!(result_type, Some(ResultType::Removed))) + { + // they were all removals so mark it as such + redacted_object.redaction_type = Some(RedactionType::Removal); + } else { + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } else { + // the result_type is empty, so we don't know what it is + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } + _ => { + // what they put in doesn't match any of the accepted values + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + } + } else { + // the method was not a string, just mark it as Unknown + redacted_object.redaction_type = Some(RedactionType::Unknown); + } + + // now we need to check if we need to do the final path substitution + match redacted_object.redaction_type { + // if you are changing what you're going to subsitute on, you need to change this. + Some(RedactionType::EmptyValue) | Some(RedactionType::PartialValue) => { + // | Some(RedactionType::ReplacementValue) => { + redacted_object.do_final_path_subsitution = true; + } + _ => { + redacted_object.do_final_path_subsitution = false; + } + } + + // put the redacted_object into the list of them + list_of_redactions.push(redacted_object); + } + + list_of_redactions +} + +// we are setting our own internal ResultType for each item that is found in the jsonPath +pub fn set_result_type_from_json_path(u: Value, item: &mut RedactedObject) -> RedactedObject { + if let Some(path) = item.original_path.as_deref() { + let path = path.trim_matches('"'); // Remove double quotes + match JsonPathInst::from_str(path) { + Ok(json_path) => { + let finder = JsonPathFinder::new(Box::new(u.clone()), Box::new(json_path)); + let matches = finder.find_as_path(); + + if let Value::Array(paths) = matches { + if paths.is_empty() { + item.result_type.push(Some(ResultType::Removed)); + } else { + // get the length of paths + let len = paths.len(); + // set the path_index_length to the length of the paths + item.path_index_count = len as i32; + for path_value in paths { + if let Value::String(found_path) = path_value { + item.final_path.push(Some(found_path.clone())); // Push found_path to final_path on the redacted object + let no_value = Value::String("NO_VALUE".to_string()); + let json_pointer = convert_to_json_pointer_path(&found_path); + let value_at_path = u.pointer(&json_pointer).unwrap_or(&no_value); + if value_at_path.is_string() { + let str_value = value_at_path.as_str().unwrap_or(""); + if str_value == "NO_VALUE" { + item.result_type.push(Some(ResultType::StringNoValue)); + } else if str_value.is_empty() { + item.result_type.push(Some(ResultType::EmptyString)); + } else { + item.result_type.push(Some(ResultType::PartialString)); + } + } else if value_at_path.is_null() { + item.result_type.push(Some(ResultType::FoundNull)); + } else if value_at_path.is_array() { + item.result_type.push(Some(ResultType::Array)); + } else if value_at_path.is_object() { + item.result_type.push(Some(ResultType::Object)); + } else { + item.result_type.push(Some(ResultType::FoundNothing)); + } + } else { + item.result_type.push(Some(ResultType::FoundUnknown)); + } + } + } + } else { + item.result_type + .push(Some(ResultType::FoundPathReturnedBadValue)); + } + } + Err(_e) => { + // siliently fail??? + // dbg!("Failed to parse JSON path '{}': {}", path, e); + } + } + } + item.clone() +} + +#[cfg(test)] +#[allow(non_snake_case)] +mod tests { + use serde_json::Value; + use std::error::Error; + use std::fs::File; + use std::io::Read; + + fn process_redacted_file(file_path: &str) -> Result> { + let mut file = File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + + // this has to be setup very specifically, just like replace_redacted_items is setup. + let mut rdap_json_response: Value = serde_json::from_str(&contents)?; + let redacted_array_option = rdap_json_response["redacted"].as_array().cloned(); + // we are testing parse_redacted_json here -- just the JSON transforms + crate::md::redacted::parse_redacted_json( + &mut rdap_json_response, + redacted_array_option.as_ref(), + ); + + let pretty_json = serde_json::to_string_pretty(&rdap_json_response)?; + println!("{}", pretty_json); + Ok(pretty_json) + } + + #[test] + fn test_process_empty_value() { + let expected_output = + std::fs::read_to_string("src/test_files/example-1_empty_value-expected.json").unwrap(); + let output = process_redacted_file("src/test_files/example-1_empty_value.json").unwrap(); + assert_eq!(output, expected_output); + } + + #[test] + fn test_process_partial_value() { + let expected_output = + std::fs::read_to_string("src/test_files/example-2_partial_value-expected.json") + .unwrap(); + let output = process_redacted_file("src/test_files/example-2_partial_value.json").unwrap(); + assert_eq!(output, expected_output); + } + + #[test] + fn test_process_dont_replace_number() { + let expected_output = std::fs::read_to_string( + "src/test_files/example-3-dont_replace_redaction_of_a_number.json", + ) + .unwrap(); + // we don't need an expected for this one, it should remain unchanged + let output = process_redacted_file( + "src/test_files/example-3-dont_replace_redaction_of_a_number.json", + ) + .unwrap(); + assert_eq!(output, expected_output); + } +} diff --git a/icann-rdap-client/src/test_files/example-1_empty_value-expected.json b/icann-rdap-client/src/test_files/example-1_empty_value-expected.json new file mode 100644 index 0000000..db51d65 --- /dev/null +++ b/icann-rdap-client/src/test_files/example-1_empty_value-expected.json @@ -0,0 +1,259 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "redacted" + ], + "objectClassName": "domain", + "ldhName": "example-1.net", + "secureDNS": { + "delegationSigned": false + }, + "notices": [ + { + "title": "Terms of Use", + "description": [ + "Service subject to Terms of Use." + ], + "links": [ + { + "rel": "self", + "href": "https://www.example.com/terms-of-use", + "type": "text/html", + "value": "https://www.example.com/terms-of-use" + } + ] + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", + "ldhName": "ns1.example.com" + }, + { + "objectClassName": "nameserver", + "ldhName": "ns2.example.com" + } + ], + "entities": [ + { + "objectClassName": "entity", + "handle": "123", + "roles": [ + "registrar" + ], + "publicIds": [ + { + "type": "IANA Registrar ID", + "identifier": "1" + } + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Example Registrar Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 100", + "123 Example Dr.", + "Dulles", + "VA", + "20166-6503", + "US" + ] + ], + [ + "email", + {}, + "text", + "contact@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ], + [ + "tel", + { + "type": "fax" + }, + "uri", + "tel:+1.7035555556" + ] + ] + ], + "entities": [ + { + "objectClassName": "entity", + "roles": [ + "abuse" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Abuse Contact" + ], + [ + "email", + {}, + "text", + "abuse@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ] + ] + ] + } + ] + }, + { + "objectClassName": "entity", + "handle": "XXXX", + "roles": [ + "registrant" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "*REDACTED*" + ], + [ + "adr", + {}, + "text", + [ + "", + "", + "", + "", + "QC", + "", + "Canada" + ] + ] + ] + ] + }, + { + "objectClassName": "entity", + "handle": "YYYY", + "roles": [ + "technical" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "" + ], + [ + "org", + {}, + "text", + "Example Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 1234", + "4321 Rue Somewhere", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ] + ] + ] + } + ], + "events": [ + { + "eventAction": "registration", + "eventDate": "1997-06-03T00:00:00Z" + }, + { + "eventAction": "last changed", + "eventDate": "2020-05-28T01:35:00Z" + }, + { + "eventAction": "expiration", + "eventDate": "2021-06-03T04:00:00Z" + } + ], + "status": [ + "server delete prohibited", + "server update prohibited", + "server transfer prohibited", + "client transfer prohibited" + ], + "redacted": [ + { + "name": { + "description": "Registrant Name" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]", + "pathLang": "jsonpath", + "method": "emptyValue", + "reason": { + "description": "Server policy" + } + } + ] +} \ No newline at end of file diff --git a/icann-rdap-client/src/test_files/example-1_empty_value.json b/icann-rdap-client/src/test_files/example-1_empty_value.json new file mode 100644 index 0000000..d998719 --- /dev/null +++ b/icann-rdap-client/src/test_files/example-1_empty_value.json @@ -0,0 +1,240 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "redacted" + ], + "objectClassName": "domain", + "ldhName": "example-1.net", + "secureDNS": { "delegationSigned": false }, + "notices": [ + { + "title": "Terms of Use", + "description": [ "Service subject to Terms of Use." ], + "links": [ + { + "rel": "self", + "href": "https://www.example.com/terms-of-use", + "type": "text/html", + "value": "https://www.example.com/terms-of-use" + } + ] + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", "ldhName": "ns1.example.com" }, + { + "objectClassName": "nameserver", "ldhName": "ns2.example.com" } + ], + "entities": [ + { + "objectClassName": "entity", + "handle": "123", + "roles": [ "registrar" ], + "publicIds": [ + { "type": "IANA Registrar ID", "identifier": "1" } + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Example Registrar Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 100", + "123 Example Dr.", + "Dulles", + "VA", + "20166-6503", + "US" + ] + ], + [ + "email", + {}, + "text", + "contact@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ], + [ + "tel", + { + "type": "fax" + }, + "uri", + "tel:+1.7035555556" + ] + ] + ], + "entities": [ + { + "objectClassName": "entity", + "roles": [ + "abuse" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Abuse Contact" + ], + [ + "email", + {}, + "text", + "abuse@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ] + ] + ] + } + ] + }, + { + "objectClassName": "entity", + "handle": "XXXX", + "roles": [ + "registrant" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "" + ], + [ + "adr", + {}, + "text", + [ + "", + "", + "", + "", + "QC", + "", + "Canada" + ] + ] + ] + ] + }, + { + "objectClassName": "entity", + "handle": "YYYY", + "roles": [ + "technical" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "" + ], + [ + "org", + {}, + "text", + "Example Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 1234", + "4321 Rue Somewhere", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ] + ] + ] + } + ], + "events": [ + { + "eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z" + }, + { + "eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z" + }, + { + "eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z" + } + ], + "status": [ + "server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited" + ], + "redacted": [ + { + "name": { + "description": "Registrant Name" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]", + "pathLang": "jsonpath", + "method": "emptyValue", + "reason": { + "description": "Server policy" + } + } + ] +} \ No newline at end of file diff --git a/icann-rdap-client/src/test_files/example-2_partial_value-expected.json b/icann-rdap-client/src/test_files/example-2_partial_value-expected.json new file mode 100644 index 0000000..3d6685f --- /dev/null +++ b/icann-rdap-client/src/test_files/example-2_partial_value-expected.json @@ -0,0 +1,302 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "redacted" + ], + "objectClassName": "domain", + "ldhName": "example-3.net", + "secureDNS": { + "delegationSigned": false + }, + "notices": [ + { + "title": "Terms of Use", + "description": [ + "Service subject to Terms of Use." + ], + "links": [ + { + "rel": "self", + "href": "https://www.example.com/terms-of-use", + "type": "text/html", + "value": "https://www.example.com/terms-of-use" + } + ] + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", + "ldhName": "ns1.example.com" + }, + { + "objectClassName": "nameserver", + "ldhName": "ns2.example.com" + } + ], + "entities": [ + { + "objectClassName": "entity", + "handle": "123", + "roles": [ + "registrar" + ], + "publicIds": [ + { + "type": "IANA Registrar ID", + "identifier": "1" + } + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Example Registrar Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 100", + "123 Example Dr.", + "Dulles", + "VA", + "20166-6503", + "US" + ] + ], + [ + "email", + {}, + "text", + "contact@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ], + [ + "tel", + { + "type": "fax" + }, + "uri", + "tel:+1.7035555556" + ] + ] + ], + "entities": [ + { + "objectClassName": "entity", + "roles": [ + "abuse" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Abuse Contact" + ], + [ + "email", + {}, + "text", + "abuse@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ] + ] + ] + } + ] + }, + { + "objectClassName": "entity", + "handle": "XXXX", + "roles": [ + "registrant" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "*REDACTED*" + ], + [ + "adr", + {}, + "text", + [ + "*REDACTED*", + "*REDACTED*", + "*REDACTED*", + "*REDACTED*", + "QC", + "*REDACTED*", + "Canada" + ] + ] + ] + ] + }, + { + "objectClassName": "entity", + "handle": "YYYY", + "roles": [ + "technical" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "*REDACTED*" + ], + [ + "org", + {}, + "text", + "Example Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 1234", + "4321 Rue Somewhere", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ] + ] + ] + } + ], + "events": [ + { + "eventAction": "registration", + "eventDate": "1997-06-03T00:00:00Z" + }, + { + "eventAction": "last changed", + "eventDate": "2020-05-28T01:35:00Z" + }, + { + "eventAction": "expiration", + "eventDate": "2021-06-03T04:00:00Z" + } + ], + "status": [ + "server delete prohibited", + "server update prohibited", + "server transfer prohibited", + "client transfer prohibited" + ], + "redacted": [ + { + "name": { + "description": "Registrant Name" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant Street" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant City" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant Postal Code" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Technical Name" + }, + "postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + } + ] +} \ No newline at end of file diff --git a/icann-rdap-client/src/test_files/example-2_partial_value.json b/icann-rdap-client/src/test_files/example-2_partial_value.json new file mode 100644 index 0000000..0024f3a --- /dev/null +++ b/icann-rdap-client/src/test_files/example-2_partial_value.json @@ -0,0 +1,283 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "redacted" + ], + "objectClassName": "domain", + "ldhName": "example-3.net", + "secureDNS": { "delegationSigned": false }, + "notices": [ + { + "title": "Terms of Use", + "description": [ "Service subject to Terms of Use." ], + "links": [ + { + "rel": "self", + "href": "https://www.example.com/terms-of-use", + "type": "text/html", + "value": "https://www.example.com/terms-of-use" + } + ] + } + ], + "nameservers": [ + { + "objectClassName": "nameserver", "ldhName": "ns1.example.com" }, + { + "objectClassName": "nameserver", "ldhName": "ns2.example.com" } + ], + "entities": [ + { + "objectClassName": "entity", + "handle": "123", + "roles": [ "registrar" ], + "publicIds": [ + { "type": "IANA Registrar ID", "identifier": "1" } + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Example Registrar Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 100", + "123 Example Dr.", + "Dulles", + "VA", + "20166-6503", + "US" + ] + ], + [ + "email", + {}, + "text", + "contact@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ], + [ + "tel", + { + "type": "fax" + }, + "uri", + "tel:+1.7035555556" + ] + ] + ], + "entities": [ + { + "objectClassName": "entity", + "roles": [ + "abuse" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Abuse Contact" + ], + [ + "email", + {}, + "text", + "abuse@organization.example" + ], + [ + "tel", + { + "type": "voice" + }, + "uri", + "tel:+1.7035555555" + ] + ] + ] + } + ] + }, + { + "objectClassName": "entity", + "handle": "XXXX", + "roles": [ + "registrant" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "" + ], + [ + "adr", + {}, + "text", + [ + "", + "", + "", + "", + "QC", + "", + "Canada" + ] + ] + ] + ] + }, + { + "objectClassName": "entity", + "handle": "YYYY", + "roles": [ + "technical" + ], + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "" + ], + [ + "org", + {}, + "text", + "Example Inc." + ], + [ + "adr", + {}, + "text", + [ + "", + "Suite 1234", + "4321 Rue Somewhere", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ] + ] + ] + } + ], + "events": [ + { + "eventAction": "registration", "eventDate": "1997-06-03T00:00:00Z" + }, + { + "eventAction": "last changed", "eventDate": "2020-05-28T01:35:00Z" + }, + { + "eventAction": "expiration", "eventDate": "2021-06-03T04:00:00Z" + } + ], + "status": [ + "server delete prohibited", "server update prohibited", "server transfer prohibited", "client transfer prohibited" + ], + "redacted": [ + { + "name": { + "description": "Registrant Name" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='fn')][3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant Street" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][:3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant City" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][3]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Registrant Postal Code" + }, + "postPath": "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='adr')][3][5]", + "pathLang": "jsonpath", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + }, + { + "name": { + "description": "Technical Name" + }, + "postPath": "$.entities[?(@.roles[0]=='technical')].vcardArray[1][?(@[0]=='fn')][3]", + "method": "partialValue", + "reason": { + "description": "Server policy" + } + } + ] +} \ No newline at end of file diff --git a/icann-rdap-client/src/test_files/example-3-dont_replace_redaction_of_a_number.json b/icann-rdap-client/src/test_files/example-3-dont_replace_redaction_of_a_number.json new file mode 100644 index 0000000..9f01e8a --- /dev/null +++ b/icann-rdap-client/src/test_files/example-3-dont_replace_redaction_of_a_number.json @@ -0,0 +1,212 @@ +{ + "rdapConformance": [ + "rdap_level_0", + "redacted" + ], + "objectClassName": "domain", + "handle": "PPP", + "ldhName": "0.2.192.in-addr.arpa", + "nameservers": [ + { + "objectClassName": "nameserver", + "ldhName": "ns1.rir.example" + }, + { + "objectClassName": "nameserver", + "ldhName": "ns2.rir.example" + } + ], + "secureDNS": { + "delegationSigned": true, + "dsData": [ + { + "keyTag": 25345, + "algorithm": 8, + "digestType": 2, + "digest": "2788970E18EA14...C890C85B8205B94" + } + ] + }, + "remarks": [ + { + "description": [ + "She sells sea shells down by the sea shore.", + "Originally written by Terry Sullivan." + ] + } + ], + "links": [ + { + "value": "https://example.net/domain/0.2.192.in-addr.arpa", + "rel": "self", + "href": "https://example.net/domain/0.2.192.in-addr.arpa", + "type": "application/rdap+json" + } + ], + "events": [ + { + "eventAction": "registration", + "eventDate": "1990-12-31T23:59:59Z" + }, + { + "eventAction": "last changed", + "eventDate": "1991-12-31T23:59:59Z", + "eventActor": "joe@example.com" + } + ], + "entities": [ + { + "objectClassName": "entity", + "handle": "XXXX", + "vcardArray": [ + "vcard", + [ + [ + "version", + {}, + "text", + "4.0" + ], + [ + "fn", + {}, + "text", + "Joe User" + ], + [ + "kind", + {}, + "text", + "individual" + ], + [ + "lang", + { + "pref": "1" + }, + "language-tag", + "fr" + ], + [ + "lang", + { + "pref": "2" + }, + "language-tag", + "en" + ], + [ + "org", + { + "type": "work" + }, + "text", + "Example" + ], + [ + "title", + {}, + "text", + "Research Scientist" + ], + [ + "role", + {}, + "text", + "Project Lead" + ], + [ + "adr", + { + "type": "work" + }, + "text", + [ + "", + "Suite 1234", + "4321 Rue Somewhere", + "Quebec", + "QC", + "G1V 2M2", + "Canada" + ] + ], + [ + "tel", + { + "type": [ + "work", + "voice" + ], + "pref": "1" + }, + "uri", + "tel:+1-555-555-1234;ext=102" + ], + [ + "email", + { + "type": "work" + }, + "text", + "joe.user@example.com" + ] + ] + ], + "roles": [ + "registrant" + ], + "remarks": [ + { + "description": [ + "She sells sea shells down by the sea shore.", + "Originally written by Terry Sullivan." + ] + } + ], + "links": [ + { + "value": "https://example.net/entity/XXXX", + "rel": "self", + "href": "https://example.net/entity/XXXX", + "type": "application/rdap+json" + } + ], + "events": [ + { + "eventAction": "registration", + "eventDate": "1990-12-31T23:59:59Z" + }, + { + "eventAction": "last changed", + "eventDate": "1991-12-31T23:59:59Z", + "eventActor": "joe@example.com" + } + ] + } + ], + "network": { + "objectClassName": "ip network", + "handle": "XXXX-RIR", + "startAddress": "192.0.2.0", + "endAddress": "192.0.2.255", + "ipVersion": "v4", + "name": "NET-RTR-1", + "type": "DIRECT ALLOCATION", + "country": "AU", + "parentHandle": "YYYY-RIR", + "status": [ + "active" + ] + }, + "redacted": [ + { + "name": { + "description": "Registrant keyTag" + }, + "postPath": "$['secureDNS']['dsData'][0]['keyTag']", + "pathLang": "jsonpath", + "method": "partialValue" + } + ] +} \ No newline at end of file