"""Secret data masking module."""importcopyimportos
[docs]classLearnedError(Exception):""" Raised when a new value is learned, to run masking again. """pass
[docs]classMask:""" 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)ifkeyselseset()self.values=set(values)ifvalueselseset()self.renderer=rendererifos.getenv('DEBUG'):self.debug=Trueelse:self.debug=debugdef__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. """ifself.debug:returndatawhileTrue:try:returnself._mask(copy.deepcopy(data))exceptLearnedError:continueelse:breakdef_mask(self,data):""" Actual, in-place masking method. """ifisinstance(data,dict):forkey,valueindata.items():ifkeyinself.keys:ifself.renderer:value=self.renderer(value)ifvaluenotinself.values:self.values.add(value)raiseLearnedError()data[key]='***MASKED***'else:data[key]=self._mask(value)elifisinstance(data,list):return[self._mask(item)foritemindata]elifisinstance(data,set):return{self._mask(item)foritemindata}elifisinstance(data,str):forvalueinsorted(self.values,key=len,reverse=True):data=data.replace(str(value),'***MASKED***')returndatadef__repr__(self):result='Mask(keys=['+', '.join(self.keys)+']'ifself.values:result+=f', number_of_values={len(self.values)}'returnresult+')'def__bool__(self):returnbool(self.keysorself.values)