Source code for cli2.mask

"""
Secret data masking module.
"""

import copy
import os


[docs] class LearnedError(Exception): """ Raised when a new value is learned, to run masking again. """ pass
[docs] class Mask: """ Masking object that can learn values. .. code-block:: python mask = cli2.Mask(keys=['password'], values=['secretval']) result = mask(dict(password='xx', text='some secretval noise xx')) Will cause result to be: .. code-block:: yaml password: ***MASKED*** text: some ***MASKED*** noise ***MASKED*** Because: - ``secretval`` was given as a value to mask - ``password``'s value because ``password`` was given as a key to match - the Mask object learned the value of the ``password`` key, and masked it in ``text`` .. py:attribute:: keys Set of keys that contain values to mask .. py:attribute:: values Set of values to mask .. py:attribute:: renderer Optionnal callback to render discovered values to mask .. py:attribute:: debug Enabled by the :envvar:`DEBUG` environment variable, makes this a no-op (don't mask anything). """ def __init__(self, keys=None, values=None, renderer=None, debug=False): self.keys = set(keys) if keys else set() self.values = set(values) if values else set() self.renderer = renderer if os.getenv('DEBUG'): self.debug = True else: self.debug = debug def __call__(self, data): """" Do our best to mask sensitive values in the data param recursively, returning a masked copy of the passed data. - when data is a dict: it is recursively iterated on, any value that in is :py:attr:`keys` will have it's value replaced with ``***MASKED***``, also, the value is added to :py:attr:`values`. - when data is a string, each :py:attr:`values` will be replaced with ``***MASKED***``, so we're actually able to mask sensitive information from stdout outputs and the likes. - when data is a list, each item is passed to :py:meth:`_mask()`. Note that the :envvar:`DEBUG` environment variable will prevent any masking at all. :param data: Any kind of data to mask, will return a deepcopy of that. """ if self.debug: return data while True: try: return self._mask(copy.deepcopy(data)) except LearnedError: continue else: break def _mask(self, data): """ Actual, in-place masking method. """ if isinstance(data, dict): for key, value in data.items(): if key in self.keys: if self.renderer: value = self.renderer(value) if value not in self.values: self.values.add(value) raise LearnedError() data[key] = '***MASKED***' else: data[key] = self._mask(value) elif isinstance(data, list): return [self._mask(item) for item in data] elif isinstance(data, set): return {self._mask(item) for item in data} elif isinstance(data, str): for value in sorted(self.values, key=len, reverse=True): data = data.replace(str(value), '***MASKED***') return data def __repr__(self): result = 'Mask(keys=[' + ', '.join(self.keys) + ']' if self.values: result += f', number_of_values={len(self.values)}' return result + ')' def __bool__(self): return bool(self.keys or self.values)