Metadata-Version: 2.5
Name: python-log-redactor
Version: 0.2.0
Summary: Lightweight sensitive data redaction for accidental printing of sensitive Python strings, dicts, and logs.
Project-URL: Homepage, https://github.com/morgan-young/log-redactor
Project-URL: Repository, https://github.com/morgan-young/log-redactor
Project-URL: Issues, https://github.com/morgan-young/log-redactor/issues
Project-URL: Changelog, https://github.com/morgan-young/log-redactor/blob/main/CHANGELOG.md
Author: Morgan Young
License: MIT
License-File: LICENSE
Keywords: logging,privacy,redaction,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Log Redactor

[![CI](https://github.com/morgan-young/log-redactor/actions/workflows/ci.yml/badge.svg)](https://github.com/morgan-young/log-redactor/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/python-log-redactor.svg)](https://pypi.org/project/python-log-redactor/)
[![Python](https://img.shields.io/pypi/pyversions/python-log-redactor.svg)](https://pypi.org/project/python-log-redactor/)
[![License](https://img.shields.io/pypi/l/python-log-redactor.svg)](https://github.com/morgan-young/log-redactor/blob/main/LICENSE)

This is a small redaction helper for Python logs and payloads that helps prevent accidental exposure, via logging, of secrets and other things you wouldn't want to persist on a server somewhere (email addresses, passwords, etc.).

Of course, you may be thinking we should never log this stuff anyway, but there are some instances where things are okay when working locally but not okay when working with real infrastructure. Unfortunately, this means that some things that shouldn't get logged, do get logged, by mistake. This package tries to stop that.

It works with both the official Python logger and with print statements.

Install the PyPI package `python-log-redactor`, then `import log_redactor`. The GitHub repo is [log-redactor](https://github.com/morgan-young/log-redactor).

## Why use it?

- Redacts by key name and regex for common secrets/sensitive information
- Supports nested `dict` / `list` / `tuple` structures
- Works with standard library `logging` and `%s`-style args
- Keeps runtime dependencies at zero (stdlib only)

## Installation

```bash
pip install python-log-redactor
```

## Quick start

The usual setup is once at process start: put `RedactingFilter` on your logging handlers, then forget about it. Leave `patterns` and `keys` unset so every built-in detector runs. Attaching to the logging handlers themselves (not only a named logger) means library logs that pass through those handlers are redacted too.

```python
import logging
from log_redactor import RedactingFilter

logging.basicConfig(level=logging.INFO)
for handler in logging.getLogger().handlers:
    handler.addFilter(RedactingFilter())

logging.info("User %s used key %s", "alice@example.com", "sk-live-abc123")
# User [REDACTED] used key [REDACTED]
```

That is the core use case. The rest of the API is for when you already have a string or dict in hand, or you want the same treatment for `print`.

```python
from log_redactor import redact, redact_dict

redact("Contact: dev@example.com", patterns=["email"])
# 'Contact: [REDACTED]'

redact("https://api.example.com/x?token=super-secret&id=9", patterns=["url_token"])
# 'https://api.example.com/x?token=[REDACTED]&id=9'

print(redact_dict(
    {"username": "alice", "password": "super-secret", "profile": {"email": "alice@example.com"}},
    patterns=["email"],
))
# {'username': 'alice', 'password': '[REDACTED]', 'profile': {'email': '[REDACTED]'}}
```

Print wrapping is opt-in and process-wide. Restore `print` when you are done:

```python
from log_redactor import disable_print_redaction, enable_print_redaction

enable_print_redaction(patterns=["email", "api_key"])
print("User", "alice@example.com", "used", "sk-live-abc123")
# User [REDACTED] used [REDACTED]
disable_print_redaction()
```

## How it works

Redaction uses two independent mechanisms:

- **Keys:** if a mapping key matches a sensitive name (case-insensitive), the entire value is replaced. Nested dicts, lists, and tuples are included.
- **Patterns:** string values are scanned with regex. Matches are replaced with `replacement` (default `[REDACTED]`).

`patterns=None` enables all built-in patterns. `keys=None` uses the built-in sensitive key set. Passing `keys=[...]` or `patterns=[...]` replaces those defaults with your preferred keys or pattersn. It does not merge with them. Use `patterns=[]` plus `custom_patterns=` if you want to only apply your own regexes.

Importantly, it identifies auth tokens with the identifier of `url_token` and this is the exception to whole-match replacement. It keeps the query-parameter prefix (`token=`, `api_key=`, and similar) and only redacts the value so that developers can see the type of auth token that was printed accidentally.

## API

```python
from log_redactor import (
    RedactingFilter,
    disable_print_redaction,
    enable_print_redaction,
    redact,
    redact_dict,
)
```

- `redact(text: str, patterns=None, custom_patterns=None, replacement="[REDACTED]") -> str`
- `redact_dict(data: dict, keys=None, patterns=None, custom_patterns=None, replacement="[REDACTED]") -> dict`
- `RedactingFilter(logging.Filter)`
- `enable_print_redaction(keys=None, patterns=None, custom_patterns=None, replacement="[REDACTED]") -> None`
- `disable_print_redaction() -> None`

Unknown built-in pattern names raise `ValueError`. `redact` and `redact_dict` raise `TypeError` if the top-level value is not a `str` or `dict` respectively.

### Custom patterns and keys

```python
from log_redactor import redact, redact_dict

redact(
    "order id: internal-12345",
    patterns=[],
    custom_patterns=[r"internal-\d+"],
)
# 'order id: [REDACTED]'

redact_dict(
    {"password": "letmein", "session_id": "abc", "note": "alice@example.com"},
    keys=["session_id"],
    patterns=["email"],
)
# {'password': 'letmein', 'session_id': '[REDACTED]', 'note': '[REDACTED]'}
```

In the second example, `password` is not redacted by key detection because the optional `keys=["session_id"]` replaced the built-in key set.

### Logging

Add the filter to handlers at startup (see Quick start) so every record those handlers emit is redacted. `logger.addFilter(...)` only covers records created on that logger.

`RedactingFilter` mutates `record.msg` and `record.args` in place before the record is formatted. It works with `%s`-style arguments and mapping arguments. If you log an f-string, the message is already interpolated and pattern matching still runs on that text, however key-based redaction cannot see the original objects.

`extra=` fields and exception text are **not** redacted.

### Print redaction

`enable_print_redaction()` is opt-in. It monkeypatches `builtins.print` for the entire process, so every later `print` in that process uses the wrapper, including library code and tests. Call `disable_print_redaction()` to restore the original `print` (this is safe to call even if redaction is not active). It is preferred to use `try` / `finally` so a failure cannot leave the patch in place:

```python
enable_print_redaction(patterns=["email", "api_key"])
try:
    print("User", "alice@example.com", "used", "sk-live-abc123")
finally:
    disable_print_redaction()
```

This is a convenience for accidental prints, not a substitute for `RedactingFilter` on your handlers.

## Built-in patterns

These are best-attempt regexes and I hold my hands up that these are not perfect detectors. `patterns=None` enables all of the built in regexes that log-redactor ships with.

| Name | Example match | Notes |
| --- | --- | --- |
| `email` | `alice@example.com` | Typical `user@host.tld` addresses |
| `ipv4` | `127.0.0.1` | Dotted-quad IPv4 |
| `jwt` | `eyJhbGciOi...` (three `.`-separated segments) | Header must start with `eyJ`; payload and signature must be at least 8 characters. Versions like `1.2.3` are left alone |
| `bearer_token` | `Bearer abcd...` | `Bearer` plus a token-like string |
| `api_key` | `sk-live-abc123`, `AKIAxxxxxxxxxxxxxxxx`, `AIzaxx...` | Only OpenAI-style `sk-live` / `sk-test`, AWS access key IDs, and Google `AIza` keys |
| `url_token` | `?token=secret` → `?token=[REDACTED]` | Keeps the parameter name; redacts the value |
| `credit_card_basic` | `4111111111111111`, `378282246310005` | 13–19 digit runs (Visa, Mastercard, Amex, …), optional spaces/hyphens. Replaced only if the digits pass a Luhn check |

## Built-in sensitive keys

When `keys=None`, these names (case-insensitive) cause the whole value to be replaced:

- `password`
- `passwd`
- `secret`
- `token`
- `access_token`
- `refresh_token`
- `api_key`
- `authorization`

## Development

```bash
python3 -m venv .venv
. .venv/bin/activate
pip install -e . pytest ruff
pytest
ruff check .
```

See [CONTRIBUTING.md](CONTRIBUTING.md) if you want to send a change.

## Security note

This package is intended to reduce accidental leakage, not guarantee perfect anonymisation or satisfy a compliance regime. You should not be printing secrets to logs on real infrastructure *regardless*. If you do, this is just a failsafe. It may miss values, and some patterns will over-redact. The primary use case is combatting forgetfulness when moving from local to real infra.

Don't be stupid. Do not log secrets on real infra.

To report a vulnerability, see [SECURITY.md](SECURITY.md).

## License

This package is distributed under an MIT license.

Changes are listed in [CHANGELOG.md](CHANGELOG.md).
