From d7b1881b1cf6c8f488c62222ddd04cb0c210b17f Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Sun, 6 Apr 2025 03:21:51 -0600 Subject: [PATCH 1/5] feat(data-masking): support masking of Pydantic models, dataclasses, and standard classes (#3473) --- .../utilities/data_masking/base.py | 16 +++- .../test_data_masking_input_types.py | 74 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 tests/unit/data_masking/test_data_masking_input_types.py diff --git a/aws_lambda_powertools/utilities/data_masking/base.py b/aws_lambda_powertools/utilities/data_masking/base.py index 3eed26045c2..a0e3a63cfc8 100644 --- a/aws_lambda_powertools/utilities/data_masking/base.py +++ b/aws_lambda_powertools/utilities/data_masking/base.py @@ -26,6 +26,18 @@ logger = logging.getLogger(__name__) +def prepare_data(data: Any) -> Any: + if hasattr(data, "__dataclass_fields__"): + import dataclasses + return dataclasses.asdict(data) + + if callable(getattr(data, "model_dump", None)): + return data.model_dump() + + if callable(getattr(data, "dict", None)): + return data.dict() + + return data class DataMasking: """ @@ -93,6 +105,7 @@ def encrypt( data_masker = DataMasking(provider=encryption_provider) encrypted = data_masker.encrypt({"secret": "value"}) """ + data = prepare_data(data) return self._apply_action( data=data, fields=None, @@ -135,7 +148,7 @@ def decrypt( data_masker = DataMasking(provider=encryption_provider) encrypted = data_masker.decrypt(encrypted_data) """ - + data = prepare_data(data) return self._apply_action( data=data, fields=None, @@ -184,6 +197,7 @@ def erase( Any The data with sensitive information erased or masked. """ + data = prepare_data(data) if masking_rules: return self._apply_masking_rules(data=data, masking_rules=masking_rules) else: diff --git a/tests/unit/data_masking/test_data_masking_input_types.py b/tests/unit/data_masking/test_data_masking_input_types.py new file mode 100644 index 00000000000..b9bf41dd875 --- /dev/null +++ b/tests/unit/data_masking/test_data_masking_input_types.py @@ -0,0 +1,74 @@ +import dataclasses +import pytest +from pydantic import BaseModel + +from aws_lambda_powertools.utilities.data_masking.base import DataMasking +from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING + +@pytest.fixture +def data_masker() -> DataMasking: + return DataMasking() + +# --------------------------- +# Test with a Pydantic model +# --------------------------- +class MyPydanticModel(BaseModel): + name: str + age: int + +def test_erase_on_pydantic_model(data_masker): + # GIVEN a Pydantic model instance + model_instance = MyPydanticModel(name="powertools", age=5) + + # WHEN calling erase with fields=["age"] + result = data_masker.erase(model_instance, fields=["age"]) + + # THEN the result should be a dict with the "age" field masked + assert isinstance(result, dict) + assert result["age"] == DATA_MASKING_STRING + assert result["name"] == "powertools" + + +# --------------------------- +# Test with a dataclass +# --------------------------- +@dataclasses.dataclass +class MyDataClass: + name: str + age: int + +def test_erase_on_dataclass(data_masker): + # GIVEN a dataclass instance + dc_instance = MyDataClass(name="powertools", age=5) + + # WHEN calling erase with fields=["age"] + result = data_masker.erase(dc_instance, fields=["age"]) + + # THEN the result should be a dict with the "age" field masked + assert isinstance(result, dict) + assert result["age"] == DATA_MASKING_STRING + assert result["name"] == "powertools" + + +# --------------------------- +# Test with a custom class that implements dict() +# --------------------------- +class MyCustomClass: + def __init__(self, name, age): + self.name = name + self.age = age + + def dict(self): + return {"name": self.name, "age": self.age} + +def test_erase_on_custom_class(data_masker): + # GIVEN an instance of a custom class with a dict() method + custom_instance = MyCustomClass("powertools", 5) + + # WHEN calling erase with fields=["age"] + result = data_masker.erase(custom_instance, fields=["age"]) + + # THEN the result should be a dict with the "age" field masked + assert isinstance(result, dict) + assert result["age"] == DATA_MASKING_STRING + assert result["name"] == "powertools" \ No newline at end of file From 33e0a09821b4d2236c74bc4fd39f7e659b49f80a Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Mon, 7 Apr 2025 01:28:16 -0600 Subject: [PATCH 2/5] feat(data_masking): support complex input types via robust prepare_data() with and updated tests --- .../utilities/data_masking/base.py | 65 ++++- .../test_data_masking_input_types.py | 251 +++++++++++++++--- 2 files changed, 270 insertions(+), 46 deletions(-) diff --git a/aws_lambda_powertools/utilities/data_masking/base.py b/aws_lambda_powertools/utilities/data_masking/base.py index a0e3a63cfc8..2b9c1b17bf0 100644 --- a/aws_lambda_powertools/utilities/data_masking/base.py +++ b/aws_lambda_powertools/utilities/data_masking/base.py @@ -26,19 +26,72 @@ logger = logging.getLogger(__name__) -def prepare_data(data: Any) -> Any: +def prepare_data(data: Any, _visited: set[int] | None = None) -> Any: + """ + Recursively convert complex objects into dictionaries (or simple types) so that they can be + processed by the data masking utility. This function handles: + + - Dataclasses (using dataclasses.asdict) + - Pydantic models (using model_dump) + - Custom classes with a dict() method + - Fallback to using __dict__ if available + - Recursively traverses dicts, lists, tuples, and sets + - Guards against circular references + + Parameters + ---------- + data : Any + The input data which may be a complex type. + _visited : set, optional + Internal set of visited object IDs to prevent infinite recursion on cyclic references. + + Returns + ------- + Any + A primitive type, or a recursively converted structure (dict, list, etc.) + """ + # Initialize _visited set if not provided. + if _visited is None: + _visited = set() + + # Prevent circular references by checking if the object's id has been seen. + data_id = id(data) + if data_id in _visited: + return data # Return the object as-is if it has already been processed. + _visited.add(data_id) + + # If data is a primitive type, return it directly. + if isinstance(data, (str, int, float, bool, type(None))): + return data + + # Handle dataclasses by converting them to a dictionary. if hasattr(data, "__dataclass_fields__"): import dataclasses - return dataclasses.asdict(data) + return prepare_data(dataclasses.asdict(data), _visited=_visited) + # Handle Pydantic models (Pydantic v2 uses 'model_dump'). if callable(getattr(data, "model_dump", None)): - return data.model_dump() + return prepare_data(data.model_dump(), _visited=_visited) - if callable(getattr(data, "dict", None)): - return data.dict() + # Handle custom objects that implement a dict() method (but are not already a dict). + if callable(getattr(data, "dict", None)) and not isinstance(data, dict): + return prepare_data(data.dict(), _visited=_visited) - return data + # If data is a dictionary, process both keys and values recursively. + if isinstance(data, dict): + return {prepare_data(key, _visited=_visited): prepare_data(value, _visited=_visited) + for key, value in data.items()} + + # If data is an iterable (like a list, tuple, or set), process each element recursively. + if isinstance(data, (list, tuple, set)): + return type(data)(prepare_data(item, _visited=_visited) for item in data) + # As a fallback, if the object has a __dict__, convert its attributes. + if hasattr(data, "__dict__"): + return prepare_data(vars(data), _visited=_visited) + + # If no conversion is applicable, return the data as is. + return data class DataMasking: """ The DataMasking class orchestrates erasing, encrypting, and decrypting diff --git a/tests/unit/data_masking/test_data_masking_input_types.py b/tests/unit/data_masking/test_data_masking_input_types.py index b9bf41dd875..47f64dc92e4 100644 --- a/tests/unit/data_masking/test_data_masking_input_types.py +++ b/tests/unit/data_masking/test_data_masking_input_types.py @@ -2,57 +2,177 @@ import pytest from pydantic import BaseModel -from aws_lambda_powertools.utilities.data_masking.base import DataMasking +from aws_lambda_powertools.utilities.data_masking.base import DataMasking, prepare_data from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING @pytest.fixture def data_masker() -> DataMasking: return DataMasking() + # --------------------------- -# Test with a Pydantic model +# Direct tests for prepare_data() # --------------------------- -class MyPydanticModel(BaseModel): - name: str - age: int +def test_prepare_data_primitive(): + # Primitives should be returned unchanged. + assert prepare_data("hello") == "hello" + assert prepare_data(123) == 123 + assert prepare_data(3.14) == 3.14 + assert prepare_data(True) is True + assert prepare_data(None) is None -def test_erase_on_pydantic_model(data_masker): - # GIVEN a Pydantic model instance - model_instance = MyPydanticModel(name="powertools", age=5) - - # WHEN calling erase with fields=["age"] - result = data_masker.erase(model_instance, fields=["age"]) - - # THEN the result should be a dict with the "age" field masked + +def test_prepare_data_dict_no_change(): + # A plain dict should remain unchanged. + data = {"x": "y", "z": 10} + result = prepare_data(data) assert isinstance(result, dict) - assert result["age"] == DATA_MASKING_STRING - assert result["name"] == "powertools" + assert result == data + + +def test_prepare_data_list(): + # Lists should be processed element by element. + data = [1, "a", {"b": 2}] + result = prepare_data(data) + assert isinstance(result, list) + assert result == [1, "a", {"b": 2}] + + +def test_prepare_data_tuple(): + # Tuples should be processed and returned as tuples. + data = (1, 2, {"a": 3}) + result = prepare_data(data) + assert isinstance(result, tuple) + assert result[2]["a"] == 3 + + +def test_prepare_data_set(): + # Sets should be processed and returned as sets. + data = {1, 2, 3} + result = prepare_data(data) + assert isinstance(result, set) + assert result == {1, 2, 3} + + +def test_prepare_data_dataclass(): + # Dataclasses should be converted using dataclasses.asdict. + @dataclasses.dataclass + class MyDataClass: + name: str + age: int + + instance = MyDataClass(name="delta", age=50) + result = prepare_data(instance) + assert isinstance(result, dict) + assert result["name"] == "delta" + assert result["age"] == 50 + + +def test_prepare_data_pydantic(): + # Pydantic models should be converted using model_dump. + class MyPydanticModel(BaseModel): + name: str + age: int + + instance = MyPydanticModel(name="alpha", age=30) + result = prepare_data(instance) + assert isinstance(result, dict) + assert result["name"] == "alpha" + assert result["age"] == 30 + + +def test_prepare_data_custom_class_with_dict(): + # Custom classes that implement dict() should be processed. + class MyCustom: + def __init__(self, name, age): + self.name = name + self.age = age + + def dict(self): + return {"name": self.name, "age": self.age} + + instance = MyCustom("beta", 40) + result = prepare_data(instance) + assert isinstance(result, dict) + assert result["name"] == "beta" + assert result["age"] == 40 + + +def test_prepare_data_fallback_dict_via_dunder(): + # Objects with __dict__ should be converted via vars(). + class WithDict: + def __init__(self, value): + self.value = value + + instance = WithDict(100) + result = prepare_data(instance) + assert isinstance(result, dict) + assert result["value"] == 100 + + +def test_prepare_data_nested_structure(): + # Test a nested structure mixing dataclass, Pydantic model, custom class, and dict. + @dataclasses.dataclass + class NestedDC: + x: int + y: str + + class NestedPM(BaseModel): + a: int + b: str + + class NestedCustom: + def __init__(self, z): + self.z = z + def dict(self): + return {"z": self.z} + + data = { + "dc": NestedDC(x=10, y="foo"), + "pm": NestedPM(a=5, b="bar"), + "custom": NestedCustom(z="baz"), + "nested": { + "list": [NestedDC(x=1, y="inner"), NestedPM(a=2, b="inner2")] + } + } + result = prepare_data(data) + # Assert conversions occurred. + assert isinstance(result, dict) + assert isinstance(result["dc"], dict) + assert result["dc"]["x"] == 10 + assert result["dc"]["y"] == "foo" + assert isinstance(result["pm"], dict) + assert result["pm"]["a"] == 5 + assert result["pm"]["b"] == "bar" + assert isinstance(result["custom"], dict) + assert result["custom"]["z"] == "baz" + assert isinstance(result["nested"], dict) + assert isinstance(result["nested"]["list"], list) + assert result["nested"]["list"][0]["y"] == "inner" + assert result["nested"]["list"][1]["a"] == 2 + + +def test_prepare_data_circular_reference(): + # Create a circular reference. + data = {"a": 1} + data["self"] = data + result = prepare_data(data) + assert result["a"] == 1 + assert "self" in result # --------------------------- -# Test with a dataclass +# Integration tests through DataMasking.erase() # --------------------------- +class MyPydanticModel(BaseModel): + name: str + age: int + @dataclasses.dataclass class MyDataClass: name: str age: int -def test_erase_on_dataclass(data_masker): - # GIVEN a dataclass instance - dc_instance = MyDataClass(name="powertools", age=5) - - # WHEN calling erase with fields=["age"] - result = data_masker.erase(dc_instance, fields=["age"]) - - # THEN the result should be a dict with the "age" field masked - assert isinstance(result, dict) - assert result["age"] == DATA_MASKING_STRING - assert result["name"] == "powertools" - - -# --------------------------- -# Test with a custom class that implements dict() -# --------------------------- class MyCustomClass: def __init__(self, name, age): self.name = name @@ -61,14 +181,65 @@ def __init__(self, name, age): def dict(self): return {"name": self.name, "age": self.age} +def test_erase_on_pydantic_model(data_masker): + # GIVEN a Pydantic model instance. + instance = MyPydanticModel(name="powertools", age=5) + # WHEN calling erase with fields ["age"]. + result = data_masker.erase(instance, fields=["age"]) + # THEN the "age" field is masked. + assert isinstance(result, dict) + assert result["age"] == DATA_MASKING_STRING + assert result["name"] == "powertools" + +def test_erase_on_dataclass(data_masker): + # GIVEN a dataclass instance. + instance = MyDataClass(name="powertools", age=5) + result = data_masker.erase(instance, fields=["age"]) + assert isinstance(result, dict) + assert result["age"] == DATA_MASKING_STRING + assert result["name"] == "powertools" + def test_erase_on_custom_class(data_masker): - # GIVEN an instance of a custom class with a dict() method - custom_instance = MyCustomClass("powertools", 5) - - # WHEN calling erase with fields=["age"] - result = data_masker.erase(custom_instance, fields=["age"]) - - # THEN the result should be a dict with the "age" field masked + # GIVEN a custom class instance with dict() method. + instance = MyCustomClass("powertools", 5) + result = data_masker.erase(instance, fields=["age"]) assert isinstance(result, dict) assert result["age"] == DATA_MASKING_STRING - assert result["name"] == "powertools" \ No newline at end of file + assert result["name"] == "powertools" + +def test_erase_on_nested_complex_structure(data_masker): + # GIVEN a nested structure combining multiple types. + @dataclasses.dataclass + class NestedDC: + value: int + + class NestedPM(BaseModel): + value: int + + class MyCustomClass: + def __init__(self, name, age): + self.name = name + self.age = age + + def dict(self): + return {"name": self.name, "age": self.age} + + data = { + "pydantic": NestedPM(value=10), + "dataclass": NestedDC(value=20), + "custom": MyCustomClass("example", 30), + "plain_dict": {"value": 40}, + "list": [NestedPM(value=50), {"value": 60}], + } + # Use a recursive JSONPath expression to search for any key "value" at any depth. + result = data_masker.erase(data, fields=["$..value"]) + + # Verify that in each nested dict where a "value" key exists the value is masked. + assert result["pydantic"]["value"] == DATA_MASKING_STRING + assert result["dataclass"]["value"] == DATA_MASKING_STRING + # "custom" branch remains unchanged because it doesn't contain a "value" key. + assert result["custom"] == {"name": "example", "age": 30} + assert result["plain_dict"]["value"] == DATA_MASKING_STRING + # List items that are dicts with "value" get masked. + assert result["list"][0]["value"] == DATA_MASKING_STRING + assert result["list"][1]["value"] == DATA_MASKING_STRING \ No newline at end of file From a472ec933bb659a93204de054016f4a8e65bbaab Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Mon, 7 Apr 2025 02:41:52 -0600 Subject: [PATCH 3/5] docs(data-masking): add support docs for Pydantic, dataclasses, and custom classes and updated test code --- docs/utilities/data_masking.md | 62 ++++++++++++++++++- .../test_data_masking_input_types.py | 45 +++----------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/docs/utilities/data_masking.md b/docs/utilities/data_masking.md index 1de6419c390..662a4c14758 100644 --- a/docs/utilities/data_masking.md +++ b/docs/utilities/data_masking.md @@ -117,6 +117,58 @@ Erasing will remove the original data and replace it with a `*****`. This means --8<-- "examples/data_masking/src/getting_started_erase_data_output.json" ``` +### Supported Input Types + +You can pass in different types of Python objects. Internally, we convert these to dictionaries for processing. + +Examples below show how `erase()` works with each type. + +=== "Pydantic Model" + +```python +from pydantic import BaseModel +from aws_lambda_powertools.utilities.data_masking import DataMasking + +class User(BaseModel): + name: str + age: int + +model = User(name="powertools", age=42) +masked = DataMasking().erase(model, fields=["age"]) +print(masked) # {'name': 'powertools', 'age': '*****'} +``` + +=== "Dataclass" + +```python +from dataclasses import dataclass +from aws_lambda_powertools.utilities.data_masking import DataMasking + +@dataclass +class User: + name: str + age: int + +model = User(name="powertools", age=42) +masked = DataMasking().erase(model, fields=["age"]) +print(masked) # {'name': 'powertools', 'age': '*****'} +``` + +=== "Custom Class with dict()" + +```python +class User: + def __init__(self, name, age): + self.name = name + self.age = age + def dict(self): + return {"name": self.name, "age": self.age} + +model = User("powertools", 42) +masked = DataMasking().erase(model, fields=["age"]) +print(masked) # {'name': 'powertools', 'age': '*****'} +``` + #### Custom masking The `erase` method also supports additional flags for more advanced and flexible masking: @@ -440,8 +492,14 @@ Note that the return will be a deserialized JSON and your desired fields updated ### Data serialization -???+ note "Current limitations" - 1. Python classes, `Dataclasses`, and `Pydantic models` are not supported yet. +???+ tip "Extended input support" + We now support `Pydantic models`, `Dataclasses`, and custom classes with `dict()` or `__dict__` for input. + + These types are automatically converted into dictionaries before masking, encrypting, or decrypting. + + However, please note that we don't convert the result **back** into the original object type. The returned object will be a dictionary. + + This may impact validation or schema enforcement when using tools like Pydantic. Before we traverse the data structure, we perform two important operations on input data: diff --git a/tests/unit/data_masking/test_data_masking_input_types.py b/tests/unit/data_masking/test_data_masking_input_types.py index 47f64dc92e4..9491b8f38c6 100644 --- a/tests/unit/data_masking/test_data_masking_input_types.py +++ b/tests/unit/data_masking/test_data_masking_input_types.py @@ -5,25 +5,21 @@ from aws_lambda_powertools.utilities.data_masking.base import DataMasking, prepare_data from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING + @pytest.fixture def data_masker() -> DataMasking: return DataMasking() -# --------------------------- -# Direct tests for prepare_data() -# --------------------------- def test_prepare_data_primitive(): - # Primitives should be returned unchanged. assert prepare_data("hello") == "hello" assert prepare_data(123) == 123 - assert prepare_data(3.14) == 3.14 + assert prepare_data(3.14) == pytest.approx(3.14) assert prepare_data(True) is True assert prepare_data(None) is None def test_prepare_data_dict_no_change(): - # A plain dict should remain unchanged. data = {"x": "y", "z": 10} result = prepare_data(data) assert isinstance(result, dict) @@ -31,7 +27,6 @@ def test_prepare_data_dict_no_change(): def test_prepare_data_list(): - # Lists should be processed element by element. data = [1, "a", {"b": 2}] result = prepare_data(data) assert isinstance(result, list) @@ -39,7 +34,6 @@ def test_prepare_data_list(): def test_prepare_data_tuple(): - # Tuples should be processed and returned as tuples. data = (1, 2, {"a": 3}) result = prepare_data(data) assert isinstance(result, tuple) @@ -47,7 +41,6 @@ def test_prepare_data_tuple(): def test_prepare_data_set(): - # Sets should be processed and returned as sets. data = {1, 2, 3} result = prepare_data(data) assert isinstance(result, set) @@ -55,7 +48,6 @@ def test_prepare_data_set(): def test_prepare_data_dataclass(): - # Dataclasses should be converted using dataclasses.asdict. @dataclasses.dataclass class MyDataClass: name: str @@ -69,7 +61,6 @@ class MyDataClass: def test_prepare_data_pydantic(): - # Pydantic models should be converted using model_dump. class MyPydanticModel(BaseModel): name: str age: int @@ -82,7 +73,6 @@ class MyPydanticModel(BaseModel): def test_prepare_data_custom_class_with_dict(): - # Custom classes that implement dict() should be processed. class MyCustom: def __init__(self, name, age): self.name = name @@ -99,7 +89,6 @@ def dict(self): def test_prepare_data_fallback_dict_via_dunder(): - # Objects with __dict__ should be converted via vars(). class WithDict: def __init__(self, value): self.value = value @@ -111,7 +100,6 @@ def __init__(self, value): def test_prepare_data_nested_structure(): - # Test a nested structure mixing dataclass, Pydantic model, custom class, and dict. @dataclasses.dataclass class NestedDC: x: int @@ -124,6 +112,7 @@ class NestedPM(BaseModel): class NestedCustom: def __init__(self, z): self.z = z + def dict(self): return {"z": self.z} @@ -136,24 +125,16 @@ def dict(self): } } result = prepare_data(data) - # Assert conversions occurred. - assert isinstance(result, dict) - assert isinstance(result["dc"], dict) assert result["dc"]["x"] == 10 assert result["dc"]["y"] == "foo" - assert isinstance(result["pm"], dict) assert result["pm"]["a"] == 5 assert result["pm"]["b"] == "bar" - assert isinstance(result["custom"], dict) assert result["custom"]["z"] == "baz" - assert isinstance(result["nested"], dict) - assert isinstance(result["nested"]["list"], list) assert result["nested"]["list"][0]["y"] == "inner" assert result["nested"]["list"][1]["a"] == 2 def test_prepare_data_circular_reference(): - # Create a circular reference. data = {"a": 1} data["self"] = data result = prepare_data(data) @@ -161,18 +142,17 @@ def test_prepare_data_circular_reference(): assert "self" in result -# --------------------------- -# Integration tests through DataMasking.erase() -# --------------------------- class MyPydanticModel(BaseModel): name: str age: int + @dataclasses.dataclass class MyDataClass: name: str age: int + class MyCustomClass: def __init__(self, name, age): self.name = name @@ -181,34 +161,32 @@ def __init__(self, name, age): def dict(self): return {"name": self.name, "age": self.age} + def test_erase_on_pydantic_model(data_masker): - # GIVEN a Pydantic model instance. instance = MyPydanticModel(name="powertools", age=5) - # WHEN calling erase with fields ["age"]. result = data_masker.erase(instance, fields=["age"]) - # THEN the "age" field is masked. assert isinstance(result, dict) assert result["age"] == DATA_MASKING_STRING assert result["name"] == "powertools" + def test_erase_on_dataclass(data_masker): - # GIVEN a dataclass instance. instance = MyDataClass(name="powertools", age=5) result = data_masker.erase(instance, fields=["age"]) assert isinstance(result, dict) assert result["age"] == DATA_MASKING_STRING assert result["name"] == "powertools" + def test_erase_on_custom_class(data_masker): - # GIVEN a custom class instance with dict() method. instance = MyCustomClass("powertools", 5) result = data_masker.erase(instance, fields=["age"]) assert isinstance(result, dict) assert result["age"] == DATA_MASKING_STRING assert result["name"] == "powertools" + def test_erase_on_nested_complex_structure(data_masker): - # GIVEN a nested structure combining multiple types. @dataclasses.dataclass class NestedDC: value: int @@ -231,15 +209,10 @@ def dict(self): "plain_dict": {"value": 40}, "list": [NestedPM(value=50), {"value": 60}], } - # Use a recursive JSONPath expression to search for any key "value" at any depth. result = data_masker.erase(data, fields=["$..value"]) - - # Verify that in each nested dict where a "value" key exists the value is masked. assert result["pydantic"]["value"] == DATA_MASKING_STRING assert result["dataclass"]["value"] == DATA_MASKING_STRING - # "custom" branch remains unchanged because it doesn't contain a "value" key. assert result["custom"] == {"name": "example", "age": 30} assert result["plain_dict"]["value"] == DATA_MASKING_STRING - # List items that are dicts with "value" get masked. assert result["list"][0]["value"] == DATA_MASKING_STRING assert result["list"][1]["value"] == DATA_MASKING_STRING \ No newline at end of file From cd762260d4036b6336aef67958b3418c4677cd7f Mon Sep 17 00:00:00 2001 From: Vatsal Goel <144617902+VatsalGoel3@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:19:11 -0600 Subject: [PATCH 4/5] docs(data-masking): update examples to use Lambda function entry points for supported input types and updated codebase --- .../utilities/data_masking/base.py | 2 +- docs/utilities/data_masking.md | 42 ++----------------- .../data_masking/src/lambda_mask_custom.py | 13 ++++++ .../data_masking/src/lambda_mask_dataclass.py | 12 ++++++ .../data_masking/src/lambda_mask_pydantic.py | 13 ++++++ 5 files changed, 43 insertions(+), 39 deletions(-) create mode 100644 examples/data_masking/src/lambda_mask_custom.py create mode 100644 examples/data_masking/src/lambda_mask_dataclass.py create mode 100644 examples/data_masking/src/lambda_mask_pydantic.py diff --git a/aws_lambda_powertools/utilities/data_masking/base.py b/aws_lambda_powertools/utilities/data_masking/base.py index 2b9c1b17bf0..407b68a8768 100644 --- a/aws_lambda_powertools/utilities/data_masking/base.py +++ b/aws_lambda_powertools/utilities/data_masking/base.py @@ -9,6 +9,7 @@ import functools import logging import warnings +import dataclasses from copy import deepcopy from typing import TYPE_CHECKING, Any, Callable, Mapping, Sequence @@ -66,7 +67,6 @@ def prepare_data(data: Any, _visited: set[int] | None = None) -> Any: # Handle dataclasses by converting them to a dictionary. if hasattr(data, "__dataclass_fields__"): - import dataclasses return prepare_data(dataclasses.asdict(data), _visited=_visited) # Handle Pydantic models (Pydantic v2 uses 'model_dump'). diff --git a/docs/utilities/data_masking.md b/docs/utilities/data_masking.md index 662a4c14758..bfbc1cfc187 100644 --- a/docs/utilities/data_masking.md +++ b/docs/utilities/data_masking.md @@ -125,49 +125,15 @@ Examples below show how `erase()` works with each type. === "Pydantic Model" -```python -from pydantic import BaseModel -from aws_lambda_powertools.utilities.data_masking import DataMasking - -class User(BaseModel): - name: str - age: int - -model = User(name="powertools", age=42) -masked = DataMasking().erase(model, fields=["age"]) -print(masked) # {'name': 'powertools', 'age': '*****'} -``` +--8<-- "examples/data_masking/src/lambda_mask_pydantic.py" === "Dataclass" -```python -from dataclasses import dataclass -from aws_lambda_powertools.utilities.data_masking import DataMasking - -@dataclass -class User: - name: str - age: int - -model = User(name="powertools", age=42) -masked = DataMasking().erase(model, fields=["age"]) -print(masked) # {'name': 'powertools', 'age': '*****'} -``` +--8<-- "examples/data_masking/src/lambda_mask_dataclass.py" === "Custom Class with dict()" -```python -class User: - def __init__(self, name, age): - self.name = name - self.age = age - def dict(self): - return {"name": self.name, "age": self.age} - -model = User("powertools", 42) -masked = DataMasking().erase(model, fields=["age"]) -print(masked) # {'name': 'powertools', 'age': '*****'} -``` +--8<-- "examples/data_masking/src/lambda_mask_custom.py" #### Custom masking @@ -493,7 +459,7 @@ Note that the return will be a deserialized JSON and your desired fields updated ### Data serialization ???+ tip "Extended input support" - We now support `Pydantic models`, `Dataclasses`, and custom classes with `dict()` or `__dict__` for input. + We support `Pydantic models`, `Dataclasses`, and custom classes with `dict()` or `__dict__` for input. These types are automatically converted into dictionaries before masking, encrypting, or decrypting. diff --git a/examples/data_masking/src/lambda_mask_custom.py b/examples/data_masking/src/lambda_mask_custom.py new file mode 100644 index 00000000000..ffa63cfa65c --- /dev/null +++ b/examples/data_masking/src/lambda_mask_custom.py @@ -0,0 +1,13 @@ +class User: + def __init__(self, name, age): + self.name = name + self.age = age + + def dict(self): + return {"name": self.name, "age": self.age} + +def lambda_handler(event, context): + from aws_lambda_powertools.utilities.data_masking import DataMasking + user = User("powertools", 42) + masked = DataMasking().erase(user, fields=["age"]) + return masked \ No newline at end of file diff --git a/examples/data_masking/src/lambda_mask_dataclass.py b/examples/data_masking/src/lambda_mask_dataclass.py new file mode 100644 index 00000000000..d1c4301e9fe --- /dev/null +++ b/examples/data_masking/src/lambda_mask_dataclass.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from aws_lambda_powertools.utilities.data_masking import DataMasking + +@dataclass +class User: + name: str + age: int + +def lambda_handler(event, context): + user = User(name="powertools", age=42) + masked = DataMasking().erase(user, fields=["age"]) + return masked \ No newline at end of file diff --git a/examples/data_masking/src/lambda_mask_pydantic.py b/examples/data_masking/src/lambda_mask_pydantic.py new file mode 100644 index 00000000000..58965b3058e --- /dev/null +++ b/examples/data_masking/src/lambda_mask_pydantic.py @@ -0,0 +1,13 @@ +from pydantic import BaseModel +from aws_lambda_powertools.utilities.data_masking import DataMasking + +class User(BaseModel): + name: str + age: int + +def lambda_handler(event, context): + # Create a sample User instance + user = User(name="powertools", age=42) + # Erase the 'age' field + masked = DataMasking().erase(user, fields=["age"]) + return masked \ No newline at end of file From 095f625094cc191c93ee110991d4a16efcc55fb6 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Fri, 11 Apr 2025 15:07:32 +0100 Subject: [PATCH 5/5] refactoring prepare_data method --- docs/utilities/data_masking.md | 54 +++++++++---------- ...custom.py => working_with_custom_types.py} | 10 ++-- ...ass.py => working_with_dataclass_types.py} | 5 +- ...ntic.py => working_with_pydantic_types.py} | 7 ++- provenance/3.9.1a8/multiple.intoto.jsonl | 1 + 5 files changed, 38 insertions(+), 39 deletions(-) rename examples/data_masking/src/{lambda_mask_custom.py => working_with_custom_types.py} (61%) rename examples/data_masking/src/{lambda_mask_dataclass.py => working_with_dataclass_types.py} (74%) rename examples/data_masking/src/{lambda_mask_pydantic.py => working_with_pydantic_types.py} (63%) create mode 100644 provenance/3.9.1a8/multiple.intoto.jsonl diff --git a/docs/utilities/data_masking.md b/docs/utilities/data_masking.md index bfbc1cfc187..5abcc185938 100644 --- a/docs/utilities/data_masking.md +++ b/docs/utilities/data_masking.md @@ -117,24 +117,6 @@ Erasing will remove the original data and replace it with a `*****`. This means --8<-- "examples/data_masking/src/getting_started_erase_data_output.json" ``` -### Supported Input Types - -You can pass in different types of Python objects. Internally, we convert these to dictionaries for processing. - -Examples below show how `erase()` works with each type. - -=== "Pydantic Model" - ---8<-- "examples/data_masking/src/lambda_mask_pydantic.py" - -=== "Dataclass" - ---8<-- "examples/data_masking/src/lambda_mask_dataclass.py" - -=== "Custom Class with dict()" - ---8<-- "examples/data_masking/src/lambda_mask_custom.py" - #### Custom masking The `erase` method also supports additional flags for more advanced and flexible masking: @@ -461,24 +443,38 @@ Note that the return will be a deserialized JSON and your desired fields updated ???+ tip "Extended input support" We support `Pydantic models`, `Dataclasses`, and custom classes with `dict()` or `__dict__` for input. - These types are automatically converted into dictionaries before masking, encrypting, or decrypting. - - However, please note that we don't convert the result **back** into the original object type. The returned object will be a dictionary. - - This may impact validation or schema enforcement when using tools like Pydantic. + These types are automatically converted into dictionaries before `masking` and `encrypting` operations. Please not that we **don't convert back** to the original type, and the returned object will be a dictionary. Before we traverse the data structure, we perform two important operations on input data: 1. If `JSON string`, **deserialize** using default or provided deserializer. -2. If `dictionary`, **normalize** into `JSON` to prevent traversing unsupported data types. - -When decrypting, we revert the operation to restore the original data structure. +2. If `dictionary or complex types`, **normalize** into `JSON` to prevent traversing unsupported data types. For compatibility or performance, you can optionally pass your own JSON serializer and deserializer to replace `json.dumps` and `json.loads` respectively: -```python hl_lines="17-18" title="advanced_custom_serializer.py" ---8<-- "examples/data_masking/src/advanced_custom_serializer.py" -``` +=== "Working with custom types" + + ```python + --8<-- "examples/data_masking/src/working_with_custom_types.py" + ``` + +=== "Working with Pydantic" + + ```python + --8<-- "examples/data_masking/src/working_with_pydantic_types.py" + ``` + +=== "Working with dataclasses" + + ```python + --8<-- "examples/data_masking/src/working_with_dataclass_types.py" + ``` + +=== "Working with serializer" + + ```python + --8<-- "examples/data_masking/src/advanced_custom_serializer.py" + ``` ### Using multiple keys diff --git a/examples/data_masking/src/lambda_mask_custom.py b/examples/data_masking/src/working_with_custom_types.py similarity index 61% rename from examples/data_masking/src/lambda_mask_custom.py rename to examples/data_masking/src/working_with_custom_types.py index ecc11558ae1..833fe3465ec 100644 --- a/examples/data_masking/src/lambda_mask_custom.py +++ b/examples/data_masking/src/working_with_custom_types.py @@ -1,3 +1,8 @@ +from aws_lambda_powertools.utilities.data_masking import DataMasking + +data_masker = DataMasking() + + class User: def __init__(self, name, age): self.name = name @@ -8,8 +13,5 @@ def dict(self): def lambda_handler(event, context): - from aws_lambda_powertools.utilities.data_masking import DataMasking - user = User("powertools", 42) - masked = DataMasking().erase(user, fields=["age"]) - return masked + return data_masker.erase(user, fields=["age"]) diff --git a/examples/data_masking/src/lambda_mask_dataclass.py b/examples/data_masking/src/working_with_dataclass_types.py similarity index 74% rename from examples/data_masking/src/lambda_mask_dataclass.py rename to examples/data_masking/src/working_with_dataclass_types.py index b3f75c56eb7..bcd9b13de6d 100644 --- a/examples/data_masking/src/lambda_mask_dataclass.py +++ b/examples/data_masking/src/working_with_dataclass_types.py @@ -2,6 +2,8 @@ from aws_lambda_powertools.utilities.data_masking import DataMasking +data_masker = DataMasking() + @dataclass class User: @@ -11,5 +13,4 @@ class User: def lambda_handler(event, context): user = User(name="powertools", age=42) - masked = DataMasking().erase(user, fields=["age"]) - return masked + return data_masker.erase(user, fields=["age"]) diff --git a/examples/data_masking/src/lambda_mask_pydantic.py b/examples/data_masking/src/working_with_pydantic_types.py similarity index 63% rename from examples/data_masking/src/lambda_mask_pydantic.py rename to examples/data_masking/src/working_with_pydantic_types.py index e40f4f4226f..b9f3db293b5 100644 --- a/examples/data_masking/src/lambda_mask_pydantic.py +++ b/examples/data_masking/src/working_with_pydantic_types.py @@ -2,6 +2,8 @@ from aws_lambda_powertools.utilities.data_masking import DataMasking +data_masker = DataMasking() + class User(BaseModel): name: str @@ -9,8 +11,5 @@ class User(BaseModel): def lambda_handler(event, context): - # Create a sample User instance user = User(name="powertools", age=42) - # Erase the 'age' field - masked = DataMasking().erase(user, fields=["age"]) - return masked + return data_masker.erase(user, fields=["age"]) diff --git a/provenance/3.9.1a8/multiple.intoto.jsonl b/provenance/3.9.1a8/multiple.intoto.jsonl new file mode 100644 index 00000000000..eee32730f6c --- /dev/null +++ b/provenance/3.9.1a8/multiple.intoto.jsonl @@ -0,0 +1 @@ +{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json", "verificationMaterial":{"certificate":{"rawBytes":"MIIHZzCCBu2gAwIBAgIUHkRrzhWX/GS5R+2hd/sFFE+BxOAwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwNDA3MDgwODEyWhcNMjUwNDA3MDgxODEyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZKX1kjEO+ekYjsBbi1U1L/QDYnkrcql99DFIkLqOlaT4rtBun8/spT0VGhU4D/ETQJFSLBmIBq4PdBmAMlmoXKOCBgwwggYIMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUHVQdnCW9nsdgYpGRKK9Pq/8pTe8wHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgYQGA1UdEQEB/wR6MHiGdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4xLjAwOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTAWBgorBgEEAYO/MAECBAhzY2hlZHVsZTA2BgorBgEEAYO/MAEDBChjYzQ3OWIwMWQxYjIxNTdjZTI4ZDczNmU2NTdhODVjMjNjYjgwNTY1MBkGCisGAQQBg78wAQQEC1ByZS1SZWxlYXNlMDUGCisGAQQBg78wAQUEJ2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbjAgBgorBgEEAYO/MAEGBBJyZWZzL2hlYWRzL2RldmVsb3AwOwYKKwYBBAGDvzABCAQtDCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMIGGBgorBgEEAYO/MAEJBHgMdmh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvLmdpdGh1Yi93b3JrZmxvd3MvZ2VuZXJhdG9yX2dlbmVyaWNfc2xzYTMueW1sQHJlZnMvdGFncy92Mi4xLjAwOAYKKwYBBAGDvzABCgQqDChmN2RkOGM1NGMyMDY3YmFmYzEyY2E3YTU1NTk1ZDVlZTliNzUyMDRhMB0GCisGAQQBg78wAQsEDwwNZ2l0aHViLWhvc3RlZDBKBgorBgEEAYO/MAEMBDwMOmh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24wOAYKKwYBBAGDvzABDQQqDChjYzQ3OWIwMWQxYjIxNTdjZTI4ZDczNmU2NTdhODVjMjNjYjgwNTY1MCIGCisGAQQBg78wAQ4EFAwScmVmcy9oZWFkcy9kZXZlbG9wMBkGCisGAQQBg78wAQ8ECwwJMjIxOTE5Mzc5MDEGCisGAQQBg78wARAEIwwhaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzMBkGCisGAQQBg78wAREECwwJMTI5MTI3NjM4MH8GCisGAQQBg78wARIEcQxvaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi8uZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9kZXZlbG9wMDgGCisGAQQBg78wARMEKgwoY2M0NzliMDFkMWIyMTU3Y2UyOGQ3MzZlNjU3YTg1YzIzY2I4MDU2NTAYBgorBgEEAYO/MAEUBAoMCHNjaGVkdWxlMG4GCisGAQQBg78wARUEYAxeaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9hY3Rpb25zL3J1bnMvMTQzMDM5NDUwODAvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABlg9K9a4AAAQDAEgwRgIhAPQVYg6WhNN9ebd9wecA1BKGJs6ulELGByGcbXOZHoS/AiEAjtflPA+bkgkhkgvVW/syBgfoyPQQqb5by5oBUtTtoZ0wCgYIKoZIzj0EAwMDaAAwZQIxAO0r4KaIUdp6UzUD74rpJomw5NUEjNnr8+TMU8XlOty4pX4O2334t67eqJ4OVFpJRwIwCBRvAeVoeNF0raYbS9KW0ac7BUG37gwrByyAl4nwJsnwT9AiqYC8HVdOK0JSaIGq"}, "tlogEntries":[{"logIndex":"193166369", "logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}, "kindVersion":{"kind":"dsse", "version":"0.0.1"}, "integratedTime":"1744013293", "inclusionPromise":{"signedEntryTimestamp":"MEYCIQD6UVX0U4bG2ZLaIpPJlAfpGF+37EtM4huyCCYA448QFwIhAJpt4e5aKaxgfFwQTA0ItkeUWHxW32YedOL2yiuWxWcj"}, "inclusionProof":{"logIndex":"71262107", "rootHash":"RcKR1WU4oysmHF4KXfEeTzPeESGwAxzYJRiaP6jNYs0=", "treeSize":"71262108", "hashes":["MIzgC3jDsrqTqd+jz0ht8+6Wa3vZnFesPrIzVLBDlWQ=", "iHiXstjKSkJxYHnfxcYMyu8FUTSrHrX6VeHew8NcHRw=", "PcmIOXKty9iN2bYkgnVrJ6hUz1eDZtTRPl4/5DEF29g=", "oyNrDLkNI2cAcBxK4U+4bMmgADH3izSRf5vLD6QnWNo=", "wql1qcxY+4grxnGpt8zjVOE86xHFEh3uM4SGpPnjy6Y=", "uAjEEEydgrnfyR+jXZMg9APWfRS2AEje++wxGb39EHA=", "YsI0ZJueOjNd9UY3iMr3um97NPoThSYfwz9aFzsMVK4=", "fShD00Qwbyp/z8rS1ly/DRtbx9YS7CXkOH424RRMfy4=", "heR49QE3j2ZMofhhhKRvwtAnVn+e5FnEachLDJB4bbE=", "7sQMltkHocxAEtSTwfydJT3DwQoNFi2gQVDppukZJkY=", "fbcDvFWxxvvXBFyLKrnYnHFg8qUKHTgY/SMAl9UerpY=", "WeMimyaUVpdLxfKcgHbgyus6ewR2L1dlzdZW7Df5ax8=", "BPcKCT6XFebKRdSgGfXWOSnuMVAoYlKoChg1mAVeDKk=", "6Nxz6uhbPIee9Np3j+GbPrtWcIUMKWV3JVuHKO+lKN8=", "BrIdACjySNUY3ziaNg0dSpzP6w13Lmo3iw11dBQBkXk=", "8k5uuLrcciIjuShVDkTHUWyh1g+zYYW5wml3FH7EdB4=", "C2a68tJEURTNteL5zYmjaa205qVnkObfZhjeUxj5i1g=", "7v8qPHNDLerpduaMx06eb/MwgoQwczTn/cYGKX/9wZ4="], "checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n71262108\nRcKR1WU4oysmHF4KXfEeTzPeESGwAxzYJRiaP6jNYs0=\n\n— rekor.sigstore.dev wNI9ajBFAiEA6hf779CMntXDRRDwJEDxEXAsnsfLqN2C222I3jzOquYCIGXuNHVI5hb/B56bN0yaDpOTvCe5OwfDVghlLzz+8H7m\n"}}, "canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiMjQ5YjllNDA2NjEwNDAyM2RhYmRhMzQ1NmZlYzUwMGM0Mzg0ZWIwYTlkMDhkNGQzOGQwMmZhN2U3OGIwYWY5MiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImE2MzZhZGJmNjNhZjViMzJlODE3ZDdkZTZmYzYyZjNhYmQ0MWUzZDIwZTVjMWRjYTExNzNmNDQ5NDhlYzAzMjcifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVRQ0lBTjBnb0FxUlJxZTk1djNtSXJlTXZJZkFsaHlvL3NRM3QrTVlZc2NHT2szQWlCeHlIZDJHZFpvcENZMXI1UlM4QVZDWFIybTFHNjNhRzd5cVlPMjBkNnFEdz09IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VoYWVrTkRRblV5WjBGM1NVSkJaMGxWU0d0U2NucG9WMWd2UjFNMVVpc3lhR1F2YzBaR1JTdENlRTlCZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwNUVRVE5OUkdkM1QwUkZlVmRvWTA1TmFsVjNUa1JCTTAxRVozaFBSRVY1VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVmFTMWd4YTJwRlR5dGxhMWxxYzBKaWFURlZNVXd2VVVSWmJtdHlZM0ZzT1RsRVJra0thMHh4VDJ4aFZEUnlkRUoxYmpndmMzQlVNRlpIYUZVMFJDOUZWRkZLUmxOTVFtMUpRbkUwVUdSQ2JVRk5iRzF2V0V0UFEwSm5kM2RuWjFsSlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVklWbEZrQ201RFZ6bHVjMlJuV1hCSFVrdExPVkJ4THpod1ZHVTRkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMmRaVVVkQk1WVmtSVkZGUWk5M1VqWk5TR2xIWkcxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01ZW1KSVRtaE1WMXA1V1ZjeGJBcGtNamw1WVhrNWVtSklUbWhNVjJSd1pFZG9NVmxwTVc1YVZ6VnNZMjFHTUdJelNYWk1iV1J3WkVkb01WbHBPVE5pTTBweVdtMTRkbVF6VFhaYU1sWjFDbHBZU21oa1J6bDVXREprYkdKdFZubGhWMDVtWXpKNGVsbFVUWFZsVnpGelVVaEtiRnB1VFhaa1IwWnVZM2s1TWsxcE5IaE1ha0YzVDFGWlMwdDNXVUlLUWtGSFJIWjZRVUpCVVZGeVlVaFNNR05JVFRaTWVUa3dZakowYkdKcE5XaFpNMUp3WWpJMWVreHRaSEJrUjJneFdXNVdlbHBZU21waU1qVXdXbGMxTUFwTWJVNTJZbFJCVjBKbmIzSkNaMFZGUVZsUEwwMUJSVU5DUVdoNldUSm9iRnBJVm5OYVZFRXlRbWR2Y2tKblJVVkJXVTh2VFVGRlJFSkRhR3BaZWxFekNrOVhTWGROVjFGNFdXcEplRTVVWkdwYVZFazBXa1JqZWs1dFZUSk9WR1JvVDBSV2FrMXFUbXBaYW1kM1RsUlpNVTFDYTBkRGFYTkhRVkZSUW1jM09IY0tRVkZSUlVNeFFubGFVekZUV2xkNGJGbFlUbXhOUkZWSFEybHpSMEZSVVVKbk56aDNRVkZWUlVveVJqTmplVEYzWWpOa2JHTnVVblppTW5oNlRETkNkZ3BrTWxaNVpFYzVkbUpJVFhSaVIwWjBXVzFTYUV4WVFqVmtSMmgyWW1wQlowSm5iM0pDWjBWRlFWbFBMMDFCUlVkQ1FrcDVXbGRhZWt3eWFHeFpWMUo2Q2t3eVVteGtiVlp6WWpOQmQwOTNXVXRMZDFsQ1FrRkhSSFo2UVVKRFFWRjBSRU4wYjJSSVVuZGplbTkyVEROU2RtRXlWblZNYlVacVpFZHNkbUp1VFhVS1dqSnNNR0ZJVm1sa1dFNXNZMjFPZG1KdVVteGlibEYxV1RJNWRFMUpSMGRDWjI5eVFtZEZSVUZaVHk5TlFVVktRa2huVFdSdGFEQmtTRUo2VDJrNGRncGFNbXd3WVVoV2FVeHRUblppVXpsNllraE9hRXhYV25sWlZ6RnNaREk1ZVdGNU9YcGlTRTVvVEZka2NHUkhhREZaYVRGdVdsYzFiR050UmpCaU0wbDJDa3h0WkhCa1IyZ3hXV2s1TTJJelNuSmFiWGgyWkROTmRsb3lWblZhV0Vwb1pFYzVlVmd5Wkd4aWJWWjVZVmRPWm1NeWVIcFpWRTExWlZjeGMxRklTbXdLV201TmRtUkhSbTVqZVRreVRXazBlRXhxUVhkUFFWbExTM2RaUWtKQlIwUjJla0ZDUTJkUmNVUkRhRzFPTWxKclQwZE5NVTVIVFhsTlJGa3pXVzFHYlFwWmVrVjVXVEpGTTFsVVZURk9WR3N4V2tSV2JGcFViR2xPZWxWNVRVUlNhRTFDTUVkRGFYTkhRVkZSUW1jM09IZEJVWE5GUkhkM1Rsb3liREJoU0ZacENreFhhSFpqTTFKc1drUkNTMEpuYjNKQ1owVkZRVmxQTDAxQlJVMUNSSGROVDIxb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01YUdRelRYUUtZMGM1TTFwWVNqQmlNamx6WTNrNWQySXpaR3hqYmxKMllqSjRla3hYZUdoaVYwcHJXVk14ZDJWWVVtOWlNalIzVDBGWlMwdDNXVUpDUVVkRWRucEJRZ3BFVVZGeFJFTm9hbGw2VVROUFYwbDNUVmRSZUZscVNYaE9WR1JxV2xSSk5GcEVZM3BPYlZVeVRsUmthRTlFVm1wTmFrNXFXV3BuZDA1VVdURk5RMGxIQ2tOcGMwZEJVVkZDWnpjNGQwRlJORVZHUVhkVFkyMVdiV041T1c5YVYwWnJZM2s1YTFwWVdteGlSemwzVFVKclIwTnBjMGRCVVZGQ1p6YzRkMEZST0VVS1EzZDNTazFxU1hoUFZFVTFUWHBqTlUxRVJVZERhWE5IUVZGUlFtYzNPSGRCVWtGRlNYZDNhR0ZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRBcE1Na1l6WTNreGQySXpaR3hqYmxKMllqSjRlazFDYTBkRGFYTkhRVkZSUW1jM09IZEJVa1ZGUTNkM1NrMVVTVFZOVkVrelRtcE5ORTFJT0VkRGFYTkhDa0ZSVVVKbk56aDNRVkpKUldOUmVIWmhTRkl3WTBoTk5reDVPVzVoV0ZKdlpGZEpkVmt5T1hSTU1rWXpZM2t4ZDJJelpHeGpibEoyWWpKNGVrd3pRbllLWkRKV2VXUkhPWFppU0UxMFlrZEdkRmx0VW1oTVdFSTFaRWRvZG1KcE9IVmFNbXd3WVVoV2FVd3paSFpqYlhSdFlrYzVNMk41T1hkamJWVjBZMjFXY3dwYVYwWjZXbE0xTldKWGVFRmpiVlp0WTNrNWIxcFhSbXRqZVRscldsaGFiR0pIT1hkTlJHZEhRMmx6UjBGUlVVSm5OemgzUVZKTlJVdG5kMjlaTWswd0NrNTZiR2xOUkVaclRWZEplVTFVVlROWk1sVjVUMGRSTTAxNldteE9hbFV6V1ZSbk1WbDZTWHBaTWtrMFRVUlZNazVVUVZsQ1oyOXlRbWRGUlVGWlR5OEtUVUZGVlVKQmIwMURTRTVxWVVkV2EyUlhlR3hOUnpSSFEybHpSMEZSVVVKbk56aDNRVkpWUlZsQmVHVmhTRkl3WTBoTk5reDVPVzVoV0ZKdlpGZEpkUXBaTWpsMFRESkdNMk41TVhkaU0yUnNZMjVTZG1JeWVIcE1NMEoyWkRKV2VXUkhPWFppU0UxMFlrZEdkRmx0VW1oTVdFSTFaRWRvZG1KcE9XaFpNMUp3Q21JeU5YcE1NMG94WW01TmRrMVVVWHBOUkUwMVRrUlZkMDlFUVhaWldGSXdXbGN4ZDJSSVRYWk5WRUZYUW1kdmNrSm5SVVZCV1U4dlRVRkZWMEpCWjAwS1FtNUNNVmx0ZUhCWmVrTkNhWGRaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamxDU0hOQlpWRkNNMEZPTURsTlIzSkhlSGhGZVZsNGEyVklTbXh1VG5kTGFRcFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKc1p6bExPV0UwUVVGQlVVUkJSV2QzVW1kSmFFRlFVVlpaWnpaWGFFNU9PV1ZpWkRsM1pXTkJDakZDUzBkS2N6WjFiRVZNUjBKNVIyTmlXRTlhU0c5VEwwRnBSVUZxZEdac1VFRXJZbXRuYTJoclozWldWeTl6ZVVKblptOTVVRkZSY1dJMVluazFiMElLVlhSVWRHOWFNSGREWjFsSlMyOWFTWHBxTUVWQmQwMUVZVUZCZDFwUlNYaEJUekJ5TkV0aFNWVmtjRFpWZWxWRU56UnljRXB2YlhjMVRsVkZhazV1Y2dvNEsxUk5WVGhZYkU5MGVUUndXRFJQTWpNek5IUTJOMlZ4U2pSUFZrWndTbEozU1hkRFFsSjJRV1ZXYjJWT1JqQnlZVmxpVXpsTFZ6QmhZemRDVlVjekNqZG5kM0pDZVhsQmJEUnVkMHB6Ym5kVU9VRnBjVmxET0VoV1pFOUxNRXBUWVVsSGNRb3RMUzB0TFVWT1JDQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENnPT0ifV19fQ=="}]}, "dsseEnvelope":{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInN1YmplY3QiOlt7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0zLjkuMWE4LXB5My1ub25lLWFueS53aGwiLCJkaWdlc3QiOnsic2hhMjU2IjoiZTJlNzhjYzA0NmE0MjBjMjE4Y2Q2N2Q2YTU4MDc3ODZlODQ2ZGIzMWVhM2JiZjY1Y2EyMTY0MThhOGM3NzMzNyJ9fSx7Im5hbWUiOiIuL2F3c19sYW1iZGFfcG93ZXJ0b29scy0zLjkuMWE4LnRhci5neiIsImRpZ2VzdCI6eyJzaGEyNTYiOiJkMjMwMTU1ZjYyOTI5NjEyZDU1YTU0NTBjMWEzYTI0MmYxMWZkODQwODFkYWRiOWY3NjUyYjMzYzU5YTczYmE2In19XSwicHJlZGljYXRlIjp7ImJ1aWxkZXIiOnsiaWQiOiJodHRwczovL2dpdGh1Yi5jb20vc2xzYS1mcmFtZXdvcmsvc2xzYS1naXRodWItZ2VuZXJhdG9yLy5naXRodWIvd29ya2Zsb3dzL2dlbmVyYXRvcl9nZW5lcmljX3Nsc2EzLnltbEByZWZzL3RhZ3MvdjIuMS4wIn0sImJ1aWxkVHlwZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zbHNhLWZyYW1ld29yay9zbHNhLWdpdGh1Yi1nZW5lcmF0b3IvZ2VuZXJpY0B2MSIsImludm9jYXRpb24iOnsiY29uZmlnU291cmNlIjp7InVyaSI6ImdpdCtodHRwczovL2dpdGh1Yi5jb20vYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uQHJlZnMvaGVhZHMvZGV2ZWxvcCIsImRpZ2VzdCI6eyJzaGExIjoiY2M0NzliMDFkMWIyMTU3Y2UyOGQ3MzZlNjU3YTg1YzIzY2I4MDU2NSJ9LCJlbnRyeVBvaW50IjoiLmdpdGh1Yi93b3JrZmxvd3MvcHJlLXJlbGVhc2UueW1sIn0sImVudmlyb25tZW50Ijp7ImdpdGh1Yl9hY3RvciI6ImxlYW5kcm9kYW1hc2NlbmEiLCJnaXRodWJfYWN0b3JfaWQiOiI0Mjk1MTczIiwiZ2l0aHViX2Jhc2VfcmVmIjoiIiwiZ2l0aHViX2V2ZW50X25hbWUiOiJzY2hlZHVsZSIsImdpdGh1Yl9ldmVudF9wYXlsb2FkIjp7ImVudGVycHJpc2UiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vYXZhdGFycy5naXRodWJ1c2VyY29udGVudC5jb20vYi8xMjkwP3Y9NCIsImNyZWF0ZWRfYXQiOiIyMDE5LTExLTEzVDE4OjA1OjQxWiIsImRlc2NyaXB0aW9uIjoiIiwiaHRtbF91cmwiOiJodHRwczovL2dpdGh1Yi5jb20vZW50ZXJwcmlzZXMvYW1hem9uIiwiaWQiOjEyOTAsIm5hbWUiOiJBbWF6b24iLCJub2RlX2lkIjoiTURFd09rVnVkR1Z5Y0hKcGMyVXhNamt3Iiwic2x1ZyI6ImFtYXpvbiIsInVwZGF0ZWRfYXQiOiIyMDI0LTA5LTMwVDIxOjAyOjMwWiIsIndlYnNpdGVfdXJsIjoiaHR0cHM6Ly93d3cuYW1hem9uLmNvbS8ifSwib3JnYW5pemF0aW9uIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImRlc2NyaXB0aW9uIjoiIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9ldmVudHMiLCJob29rc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL29yZ3MvYXdzLXBvd2VydG9vbHMvaG9va3MiLCJpZCI6MTI5MTI3NjM4LCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9vcmdzL2F3cy1wb3dlcnRvb2xzL2lzc3VlcyIsImxvZ2luIjoiYXdzLXBvd2VydG9vbHMiLCJtZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9tZW1iZXJzey9tZW1iZXJ9Iiwibm9kZV9pZCI6Ik9fa2dET0I3SlUxZyIsInB1YmxpY19tZW1iZXJzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9wdWJsaWNfbWVtYmVyc3svbWVtYmVyfSIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scy9yZXBvcyIsInVybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vb3Jncy9hd3MtcG93ZXJ0b29scyJ9LCJyZXBvc2l0b3J5Ijp7ImFsbG93X2ZvcmtpbmciOnRydWUsImFyY2hpdmVfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24ve2FyY2hpdmVfZm9ybWF0fXsvcmVmfSIsImFyY2hpdmVkIjpmYWxzZSwiYXNzaWduZWVzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2Fzc2lnbmVlc3svdXNlcn0iLCJibG9ic191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvYmxvYnN7L3NoYX0iLCJicmFuY2hlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9icmFuY2hlc3svYnJhbmNofSIsImNsb25lX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24uZ2l0IiwiY29sbGFib3JhdG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb2xsYWJvcmF0b3Jzey9jb2xsYWJvcmF0b3J9IiwiY29tbWVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29tbWVudHN7L251bWJlcn0iLCJjb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbW1pdHN7L3NoYX0iLCJjb21wYXJlX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2NvbXBhcmUve2Jhc2V9Li4ue2hlYWR9IiwiY29udGVudHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vY29udGVudHMveytwYXRofSIsImNvbnRyaWJ1dG9yc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9jb250cmlidXRvcnMiLCJjcmVhdGVkX2F0IjoiMjAxOS0xMS0xNVQxMjoyNjoxMloiLCJjdXN0b21fcHJvcGVydGllcyI6e30sImRlZmF1bHRfYnJhbmNoIjoiZGV2ZWxvcCIsImRlcGxveW1lbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2RlcGxveW1lbnRzIiwiZGVzY3JpcHRpb24iOiJBIGRldmVsb3BlciB0b29sa2l0IHRvIGltcGxlbWVudCBTZXJ2ZXJsZXNzIGJlc3QgcHJhY3RpY2VzIGFuZCBpbmNyZWFzZSBkZXZlbG9wZXIgdmVsb2NpdHkuIiwiZGlzYWJsZWQiOmZhbHNlLCJkb3dubG9hZHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZG93bmxvYWRzIiwiZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2V2ZW50cyIsImZvcmsiOmZhbHNlLCJmb3JrcyI6NDE5LCJmb3Jrc19jb3VudCI6NDE5LCJmb3Jrc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9mb3JrcyIsImZ1bGxfbmFtZSI6ImF3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbiIsImdpdF9jb21taXRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2dpdC9jb21taXRzey9zaGF9IiwiZ2l0X3JlZnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3JlZnN7L3NoYX0iLCJnaXRfdGFnc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9naXQvdGFnc3svc2hhfSIsImdpdF91cmwiOiJnaXQ6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi5naXQiLCJoYXNfZGlzY3Vzc2lvbnMiOnRydWUsImhhc19kb3dubG9hZHMiOnRydWUsImhhc19pc3N1ZXMiOnRydWUsImhhc19wYWdlcyI6ZmFsc2UsImhhc19wcm9qZWN0cyI6dHJ1ZSwiaGFzX3dpa2kiOmZhbHNlLCJob21lcGFnZSI6Imh0dHBzOi8vZG9jcy5wb3dlcnRvb2xzLmF3cy5kZXYvbGFtYmRhL3B5dGhvbi9sYXRlc3QvIiwiaG9va3NfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaG9va3MiLCJodG1sX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJpZCI6MjIxOTE5Mzc5LCJpc190ZW1wbGF0ZSI6ZmFsc2UsImlzc3VlX2NvbW1lbnRfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaXNzdWVzL2NvbW1lbnRzey9udW1iZXJ9IiwiaXNzdWVfZXZlbnRzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2lzc3Vlcy9ldmVudHN7L251bWJlcn0iLCJpc3N1ZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vaXNzdWVzey9udW1iZXJ9Iiwia2V5c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9rZXlzey9rZXlfaWR9IiwibGFiZWxzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL2xhYmVsc3svbmFtZX0iLCJsYW5ndWFnZSI6IlB5dGhvbiIsImxhbmd1YWdlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9sYW5ndWFnZXMiLCJsaWNlbnNlIjp7ImtleSI6Im1pdC0wIiwibmFtZSI6Ik1JVCBObyBBdHRyaWJ1dGlvbiIsIm5vZGVfaWQiOiJNRGM2VEdsalpXNXpaVFF4Iiwic3BkeF9pZCI6Ik1JVC0wIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9saWNlbnNlcy9taXQtMCJ9LCJtZXJnZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbWVyZ2VzIiwibWlsZXN0b25lc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9taWxlc3RvbmVzey9udW1iZXJ9IiwibWlycm9yX3VybCI6bnVsbCwibmFtZSI6InBvd2VydG9vbHMtbGFtYmRhLXB5dGhvbiIsIm5vZGVfaWQiOiJNREV3T2xKbGNHOXphWFJ2Y25reU1qRTVNVGt6TnprPSIsIm5vdGlmaWNhdGlvbnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vbm90aWZpY2F0aW9uc3s/c2luY2UsYWxsLHBhcnRpY2lwYXRpbmd9Iiwib3Blbl9pc3N1ZXMiOjU1LCJvcGVuX2lzc3Vlc19jb3VudCI6NTUsIm93bmVyIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3UvMTI5MTI3NjM4P3Y9NCIsImV2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL2V2ZW50c3svcHJpdmFjeX0iLCJmb2xsb3dlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dlcnMiLCJmb2xsb3dpbmdfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9mb2xsb3dpbmd7L290aGVyX3VzZXJ9IiwiZ2lzdHNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS91c2Vycy9hd3MtcG93ZXJ0b29scy9naXN0c3svZ2lzdF9pZH0iLCJncmF2YXRhcl9pZCI6IiIsImh0bWxfdXJsIjoiaHR0cHM6Ly9naXRodWIuY29tL2F3cy1wb3dlcnRvb2xzIiwiaWQiOjEyOTEyNzYzOCwibG9naW4iOiJhd3MtcG93ZXJ0b29scyIsIm5vZGVfaWQiOiJPX2tnRE9CN0pVMWciLCJvcmdhbml6YXRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvb3JncyIsInJlY2VpdmVkX2V2ZW50c191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3JlY2VpdmVkX2V2ZW50cyIsInJlcG9zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvcmVwb3MiLCJzaXRlX2FkbWluIjpmYWxzZSwic3RhcnJlZF91cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzL3N0YXJyZWR7L293bmVyfXsvcmVwb30iLCJzdWJzY3JpcHRpb25zX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vdXNlcnMvYXdzLXBvd2VydG9vbHMvc3Vic2NyaXB0aW9ucyIsInR5cGUiOiJPcmdhbml6YXRpb24iLCJ1cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3VzZXJzL2F3cy1wb3dlcnRvb2xzIiwidXNlcl92aWV3X3R5cGUiOiJwdWJsaWMifSwicHJpdmF0ZSI6ZmFsc2UsInB1bGxzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3B1bGxzey9udW1iZXJ9IiwicHVzaGVkX2F0IjoiMjAyNS0wNC0wN1QwNzo0MDoyMFoiLCJyZWxlYXNlc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi9yZWxlYXNlc3svaWR9Iiwic2l6ZSI6MTA0MjA0LCJzc2hfdXJsIjoiZ2l0QGdpdGh1Yi5jb206YXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uLmdpdCIsInN0YXJnYXplcnNfY291bnQiOjMwMTgsInN0YXJnYXplcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3RhcmdhemVycyIsInN0YXR1c2VzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3N0YXR1c2VzL3tzaGF9Iiwic3Vic2NyaWJlcnNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaWJlcnMiLCJzdWJzY3JpcHRpb25fdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vc3Vic2NyaXB0aW9uIiwic3ZuX3VybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ0YWdzX3VybCI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvYXdzLXBvd2VydG9vbHMvcG93ZXJ0b29scy1sYW1iZGEtcHl0aG9uL3RhZ3MiLCJ0ZWFtc191cmwiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL2F3cy1wb3dlcnRvb2xzL3Bvd2VydG9vbHMtbGFtYmRhLXB5dGhvbi90ZWFtcyIsInRvcGljcyI6WyJhd3MiLCJhd3MtbGFtYmRhIiwiaGFja3RvYmVyZmVzdCIsImxhbWJkYSIsInB5dGhvbiIsInNlcnZlcmxlc3MiXSwidHJlZXNfdXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24vZ2l0L3RyZWVzey9zaGF9IiwidXBkYXRlZF9hdCI6IjIwMjUtMDQtMDdUMDc6NDA6MjVaIiwidXJsIjoiaHR0cHM6Ly9hcGkuZ2l0aHViLmNvbS9yZXBvcy9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob24iLCJ2aXNpYmlsaXR5IjoicHVibGljIiwid2F0Y2hlcnMiOjMwMTgsIndhdGNoZXJzX2NvdW50IjozMDE4LCJ3ZWJfY29tbWl0X3NpZ25vZmZfcmVxdWlyZWQiOnRydWV9LCJzY2hlZHVsZSI6IjAgOCAqICogMS01Iiwid29ya2Zsb3ciOiIuZ2l0aHViL3dvcmtmbG93cy9wcmUtcmVsZWFzZS55bWwifSwiZ2l0aHViX2hlYWRfcmVmIjoiIiwiZ2l0aHViX3JlZiI6InJlZnMvaGVhZHMvZGV2ZWxvcCIsImdpdGh1Yl9yZWZfdHlwZSI6ImJyYW5jaCIsImdpdGh1Yl9yZXBvc2l0b3J5X2lkIjoiMjIxOTE5Mzc5IiwiZ2l0aHViX3JlcG9zaXRvcnlfb3duZXIiOiJhd3MtcG93ZXJ0b29scyIsImdpdGh1Yl9yZXBvc2l0b3J5X293bmVyX2lkIjoiMTI5MTI3NjM4IiwiZ2l0aHViX3J1bl9hdHRlbXB0IjoiMSIsImdpdGh1Yl9ydW5faWQiOiIxNDMwMzk0NTA4MCIsImdpdGh1Yl9ydW5fbnVtYmVyIjoiMjEzIiwiZ2l0aHViX3NoYTEiOiJjYzQ3OWIwMWQxYjIxNTdjZTI4ZDczNmU2NTdhODVjMjNjYjgwNTY1In19LCJtZXRhZGF0YSI6eyJidWlsZEludm9jYXRpb25JRCI6IjE0MzAzOTQ1MDgwLTEiLCJjb21wbGV0ZW5lc3MiOnsicGFyYW1ldGVycyI6dHJ1ZSwiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlfSwicmVwcm9kdWNpYmxlIjpmYWxzZX0sIm1hdGVyaWFscyI6W3sidXJpIjoiZ2l0K2h0dHBzOi8vZ2l0aHViLmNvbS9hd3MtcG93ZXJ0b29scy9wb3dlcnRvb2xzLWxhbWJkYS1weXRob25AcmVmcy9oZWFkcy9kZXZlbG9wIiwiZGlnZXN0Ijp7InNoYTEiOiJjYzQ3OWIwMWQxYjIxNTdjZTI4ZDczNmU2NTdhODVjMjNjYjgwNTY1In19XX19", "payloadType":"application/vnd.in-toto+json", "signatures":[{"sig":"MEQCIAN0goAqRRqe95v3mIreMvIfAlhyo/sQ3t+MYYscGOk3AiBxyHd2GdZopCY1r5RS8AVCXR2m1G63aG7yqYO20d6qDw=="}]}} \ No newline at end of file