Skip to content

Commit

Permalink
allow for redaction of bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
jeking3 committed Oct 19, 2020
1 parent 9b4f012 commit 8ee3094
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 15 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

## [0.9.2] - 2020-10-19

### Changed

- Allow redaction of bytes.

## [0.9.1] - 2020-09-26

### Changed

- When starting a recording, remove any existing uncompressed file.

## [0.9.0] - 2020-09-24
Expand Down
28 changes: 19 additions & 9 deletions interposer/tapedeck.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from typing import Callable
from typing import Dict
from typing import Optional
from typing import Union

import yaml

Expand Down Expand Up @@ -175,7 +176,7 @@ def __init__(self, deck: Path, mode: Mode) -> None:
# call ordinal key (channel name) and value (ordinal number)
self._call_ordinals: Dict[str, int] = {}
self._logger = logging.getLogger(__name__)
self._redactions: Dict[str, str] = dict()
self._redactions: Dict[Union[str, bytes], str] = dict()
# the open file resource
self._tape = None

Expand Down Expand Up @@ -343,7 +344,7 @@ def playback(self, context: CallContext, channel: str = "default") -> Any:
self._log_ex("playback", context, payload.ex)
raise payload.ex

def redact(self, secret: str, identifier: str) -> str:
def redact(self, secret: Union[str, bytes], identifier: str) -> str:
"""
Auto-track secrets for redaction.
Expand All @@ -366,14 +367,14 @@ def redact(self, secret: str, identifier: str) -> str:
gets returned as the secret so the playback calls align with the
recording.
"""
if not isinstance(secret, str):
raise TypeError("secret must be a string")
if not isinstance(secret, (str, bytes)):
raise TypeError("secret must be a string or bytes")
if not isinstance(identifier, str):
raise TypeError("identifier must be a string")
if not secret:
raise AttributeError("secret cannot be an empty string")
raise AttributeError("secret cannot be empty")
if not identifier:
raise AttributeError("identifier cannot be an empty string")
raise AttributeError("identifier cannot be empty")

key = f"_redact_{identifier}"

Expand All @@ -397,7 +398,10 @@ def redact(self, secret: str, identifier: str) -> str:
raise AttributeError(
f"{identifier} was not used during recording to redact this secret"
)
return (identifier + ("_" * secretlen))[:secretlen]
result = (identifier + ("_" * secretlen))[:secretlen]
if isinstance(secret, bytes):
result = result.encode()
return result

def _advance(self, context: CallContext, channel: str) -> str:
"""
Expand Down Expand Up @@ -510,7 +514,10 @@ def _log(self, level: int, category: str, action: str, msg: str) -> None:
"""
msg = f"TAPE: {category}({action}): {msg}"
for secret, replacement in self._redactions.items():
msg = msg.replace(secret, replacement)
if isinstance(secret, str):
msg = msg.replace(secret, replacement)
else:
msg = msg.encode().replace(secret, replacement.encode()).decode()
self._logger.log(level, msg)

def _log_ex(self, action: str, context: CallContext, ex: Exception) -> None:
Expand Down Expand Up @@ -559,7 +566,10 @@ def _redact(self, entity: Any, return_bytes: bool = False) -> Any:
"""
raw = pickle.dumps(entity, protocol=self.PICKLE_PROTOCOL)
for secret, replacement in self._redactions.items():
raw = raw.replace(secret.encode(), replacement.encode())
raw = raw.replace(
secret.encode() if isinstance(secret, str) else secret,
replacement.encode(),
)
return pickle.loads(raw) if not return_bytes else raw # nosec

def _reduce_call(self, context: CallContext) -> Callable:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
description = "A code intercept wrapper with recording and playback options."
major = 0
minor = 9
patch = 1
patch = 2

# Everything below should be cookie-cutter

Expand Down
14 changes: 9 additions & 5 deletions tests/tapedeck_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ def test_redact(self):
with self.assertRaises(AttributeError):
# each identifier must be unique
uut.redact("crush", "THIS")
with self.assertRaises(AttributeError):
uut.redact("foobar".encode(), "THIS")

with TapeDeck(self.datadir / "recording", Mode.Playback) as uut:
# playback caller may not know the secret but does know the identifier
Expand All @@ -209,13 +211,14 @@ def test_redact(self):

def test_recording_secrets(self):
""" Tests automatic redaction of known secrets and use in playback """
token = str(uuid.uuid4())
token = str(uuid.uuid4()).encode()
token2 = str(uuid.uuid4())
keeper = KeeperOfFineSecrets(token)

# pretend someone created an object and made two calls where one succeeds and one raises

with TapeDeck(self.datadir / "recording", Mode.Recording) as uut:
# use encode to test redacting bytes
use_token = uut.redact(token, "REDACTED_SMALLER_THAN_ORIGINAL")
assert use_token == token
use_token2 = uut.redact(
Expand Down Expand Up @@ -250,9 +253,10 @@ def test_recording_secrets(self):

with TapeDeck(self.datadir / "recording", Mode.Playback) as uut:
# during playback the secret passed in may not be the same as during recording
# however since it was redacted, the identifier is what's important
# however since it was redacted, the identifier is what's important; if the
# original was bytes, this one has to be bytes too
redacted_token = uut.redact(
"not-the-original-token", "REDACTED_SMALLER_THAN_ORIGINAL"
"not-the-original-token".encode(), "REDACTED_SMALLER_THAN_ORIGINAL"
)
assert redacted_token != token
# the redaction will have the same length as the original secret
Expand Down Expand Up @@ -280,8 +284,8 @@ def test_recording_secrets(self):
assert uut.playback(
CallContext(call=redacted_keeper.get_token, args=(), kwargs={})
)
assert token not in str(ex.exception)
assert redacted_token in str(ex.exception)
assert token.decode() not in str(ex.exception)
assert redacted_token.decode() in str(ex.exception)
uut.dump(self.datadir / "dump.yaml")

# this identifier was never used during recording
Expand Down

0 comments on commit 8ee3094

Please sign in to comment.