Skip to main content

SafeStream-Redactor

CI PyPI Python License: MIT Ruff

Streaming PII & credential redaction for massive text files — constant O(1) memory, multi-tier detection (regex + validators + entropy + optional NER + contextual heuristics), fully customizable redaction.

Redact a 100 GB log file with the same memory footprint as a 1 KB one.

Unlike general PII engines (Presidio, scrubadub) it also detects credentials and secrets — AWS keys, GitHub/Slack/Stripe/Google/SendGrid/Twilio/npm tokens, JWTs, private-key blocks, and undocumented high-entropy secrets — in the same streaming pass.

Why?

Most PII tools load the whole document into memory. SafeStream-Redactor processes text in fixed-size chunks with an overlapping context window, so entities that straddle chunk boundaries are still caught — and memory use never grows with file size (proven by a test that redacts a generated 1 GB file under a 16 MB allocation cap).

Install

Three ways — pip, from source, or Docker:

# 1. pip (constant-memory core, zero third-party runtime deps)
pip install safestream-redactor

# optional spaCy-based PERSON/ORG/LOC detection:
pip install 'safestream-redactor[ner]'
python -m spacy download en_core_web_sm
# 2. from a clone (for development or the latest main)
git clone https://github.com/MounishSenisetty/SafeStream-Redactor
cd SafeStream-Redactor
pip install -e '.[dev]'
# 3. Docker — nothing to install locally; mount a directory and go
docker build -t safestream-redactor .
docker run --rm -v "$PWD":/data safestream-redactor \
    redact /data/input.txt -o /data/output.txt

# released multi-arch images (amd64/arm64, with SBOM + provenance) are on GHCR:
docker run --rm -v "$PWD":/data ghcr.io/mounishsenisetty/safestream-redactor \
    redact /data/input.txt -o /data/output.txt

Quickstart

CLI

# redact everything detectable, write to out.txt
safestream redact input.txt -o out.txt

# only emails + SSNs, custom replacement, plus a custom codename
safestream redact input.txt -o out.txt --types email,ssn --replace "***" --custom-word "ProjectX"

# mask all but the last 4 characters
safestream redact cards.csv -o masked.csv --mode mask --keep-last 4

# deterministic pseudonyms (same input -> same token), streaming from stdin
tail -f app.log | safestream redact - -o - --mode pseudonymize --hmac-key "$SECRET"

# redact a whole directory tree in parallel (RAM/CPU-aware worker count)
safestream redact ./logs -o ./logs_redacted --workers 8

# ...or split ONE big file across cores — output is byte-identical to the
# single-core pass (a seam cross-check guarantees no lost/duplicated text)
safestream redact huge.log -o huge_redacted.log --workers 8

# just list what would be redacted
safestream detect input.txt --json

# CI / pre-commit secret gate: non-zero exit if anything is found
safestream detect build.log --fail-on-detect

# per-type counts to stderr, plus a JSON run report for compliance filing
# (the report contains only type names, counts, and sizes — never matched text)
safestream redact input.txt -o out.txt --stats --report run.json

# non-UTF-8 source? decode it correctly (undecodable bytes are otherwise
# reported and warned about, since unscanned bytes are a coverage risk)
safestream redact legacy.log -o out.log --encoding latin-1

# generate a documented, validated starter policy to customise
safestream init-config -o policy.toml

While redacting, the CLI installs an offline network guard by default: any attempt to open a non-loopback connection through Python's socket layer raises an error. This is best-effort, defense-in-depth egress prevention for this process, its plugins, and its Python dependencies — not a hard boundary: native extensions issuing raw syscalls, a spawned subprocess, or another process on the machine are outside its reach. For a guaranteed boundary, also run with no network (docker run --network none ...) or a network namespace / seccomp policy. Pass --allow-network to disable the guard. See SECURITY.md for the full threat model.

Python API

from safestream_redactor import Redactor, RedactionPolicy, EntityType

redactor = Redactor()
redactor.redact("email bob@corp.io, ssn: 123-45-6789")
# 'email [REDACTED], ssn: [REDACTED]'

# detection only
for d in redactor.detect("card 4111 1111 1111 1111"):
    print(d.entity_type, d.confidence, d.text)

# per-type replacements
policy = RedactionPolicy(replacements={EntityType.EMAIL: "<EMAIL>"})
Redactor(policy=policy).redact("write to bob@corp.io")   # 'write to <EMAIL>'

# constant-memory file-to-file; the write is atomic (never a partial output)
# and the returned stats carry per-type counts — content-free audit evidence
stats = Redactor().redact_file("huge.log", "huge_redacted.log")
print(stats.summary())   # '3 detection(s): email=2, ssn=1'

# generator-based streaming (any iterable of text chunks)
with open("huge.log") as f:
    for clean_chunk in Redactor().redact_stream(iter(lambda: f.read(65536), "")):
        process(clean_chunk)

# parallel, RAM-aware redaction across a directory tree
from safestream_redactor.scheduler import redact_tree
redact_tree("logs/", "logs_redacted/", Redactor(), workers=8)

# split a single large file across cores; byte-identical to the sequential pass
Redactor().redact_file("huge.log", "huge_redacted.log", workers=8)

# enforce the offline guarantee around any block of code
from safestream_redactor import netguard
with netguard.enforced():
    Redactor().redact_file("huge.log", "huge_redacted.log")   # network calls now raise

Extending with plugins

Any installed package can add a detection tier by advertising an entry point — no fork required. SafeStream discovers and loads them automatically.

# in your plugin package's pyproject.toml
[project.entry-points."safestream_redactor.detectors"]
my_detector = "my_pkg.detectors:MyDetector"   # a Detector instance or zero-arg factory

A detector is anything with a name and detect(text) -> list[Detection] (the Detector protocol). Disable plugin loading with Redactor(load_plugins=False).

Policy files (TOML)

# policy.toml
[detection]
types = ["email", "ssn", "credit_card"]

[redaction]
mode = "replace"
replacement = "[GONE]"

[redaction.replacements]
email = "<EMAIL>"

[custom]
words = ["ProjectX"]
safestream redact input.txt -o out.txt --config policy.toml

Architecture

 chunks ──> [ rolling buffer + overlap window ] ──> redacted chunks
                     │
                     ▼
        ┌───────────────────────────┐
        │ Tier 1  deterministic     │  regex + validators (Luhn, SSN rules,
        │                           │  ipaddress parsing, ...) + credentials
        ├───────────────────────────┤
        │ Tier 2  statistical       │  Shannon-entropy scoring for bespoke,
        │                           │  undocumented high-entropy secrets
        ├───────────────────────────┤
        │ Tier 3  NER (optional)    │  spaCy PERSON / ORG / LOC
        ├───────────────────────────┤
        │ Tier 4  contextual        │  trigger words boost/suppress
        │                           │  confidence ("ssn:", "example", ...)
        └───────────────────────────┘
                     │
          confidence filter + overlap resolution
                     │
                     ▼
          redaction policy (replace / mask / pseudonymize / per-type)

  Scheduler: RAM/CPU-aware multiprocessing across files (safestream/scheduler.py).
  Parallel: one big file split into character-aligned ranges, output byte-identical
    to the sequential pass, seam cross-check falls back on disagreement (parallel.py).
  Offline guard: any non-loopback connection raises NetworkAccessError (netguard.py).
  Plugins: third-party tiers load from the 'safestream_redactor.detectors' entry point.

Each tier-1 pattern declares literal anchors every match must contain (AKIA, eyJ, @, ://, a digit, …); a cheap substring scan per window skips regexes that cannot match, so secret-sparse text runs ~25× faster with provably identical output (a differential test compares prefiltered and unfiltered detection).

The streaming engine keeps a rolling buffer of chunk_size + overlap characters. Only text at least overlap characters from the buffer's end is emitted each round; the tail is carried into the next round so any entity up to overlap characters long (default 4 KB) is always seen whole at least once, even when a chunk boundary cuts straight through it. An emit boundary that would split a detection retreats to the detection's start. Already-emitted text is kept (up to overlap chars) as read-only left context so the contextual tier scores identically to whole-text mode.

Detected entity types:

  • PII: email, phone, credit_card (Luhn-validated), ssn, ipv4, ipv6, plus person / org / loc with the NER extra.
  • Credentials & secrets: aws_key, github_token, slack_token, slack_webhook, stripe_key, google_api_key, sendgrid_key, twilio_key, npm_token, openai_key, anthropic_key, jwt, private_key, url_credentials (passwords embedded in connection strings — only the password is redacted, so postgres://svc:hunter2@db/app becomes postgres://svc:[REDACTED]@db/app), api_key (generic key = value assignments), and secret (undocumented high-entropy strings, on by default — disable with --no-entropy).
  • custom for user-supplied words and regexes.

Evaluation

Read this before quoting any number below. The corpora these numbers come from are synthetic and self-generated: the gold positives are emitted in the same canonical formats the detector targets, so a near-perfect score on them measures format coverage and false-positive resistance, not real-world recall. They are a regression fixture, not an accuracy claim, and a self-generated benchmark is not evidence of accuracy on your data. For a defensible accuracy number, run benchmarks/evaluate.py against an independent labeled corpus you control (see benchmarks/README.md — the Presidio research dataset, CoNLL, and the n2c2 de-identification set are the recommended targets). Expect recall on free-form prose (names, addresses) to be substantially lower than on structured PII unless the [ner] extra is enabled.

Format coverage & false-positive resistance (synthetic). On the adversarial corpus (1 MB of noisy log/JSON/CSV/SQL dense with hard negatives — order numbers, ISO timestamps, UUIDs, git hashes, invalid-area SSNs, 5-octet version strings; reproduce with benchmarks/generate_hard_dataset.py then benchmarks/run_benchmark.py):

Tool Precision Recall* Throughput
safestream-redactor 0.999 1.000 ~3.1 MB/s
Microsoft Presidio (patterns) 0.649 0.843 ~0.06 MB/s

* Recall here is against self-generated positives and is not a real-world recall figure — see the note above. The meaningful, non-circular result in this table is precision under distractors: SafeStream holds 0.999 while Presidio's phone recognizer emits thousands of false positives (P=0.649). Presidio was run via its pattern recognizers; its spaCy NER tier is a separate, model-dependent path that will out-recall SafeStream's regex core on prose unless you enable [ner].

Throughput is a known limitation, not a strength. ~3 MB/s single-core means a 100 GB file takes hours; compiled scanners (gitleaks, ripgrep-class) are 100–1000× faster. The value here is constant memory and credential coverage, not raw speed. A single large file can now be split across cores with --workers N / redact_file(..., workers=N) — measured ~3.7× on 4 cores, with byte-identical output to the single-core pass (a seam cross-check falls back to sequential rather than risk differing output). That narrows but does not close the gap to compiled scanners; a Vectorscan/re2 backend for the remaining constant factor stays on the roadmap.

Credentials & secrets — the genuinely differentiating capability. On a corpus of AWS keys, GitHub/Slack/Stripe/Google/SendGrid/npm tokens, JWTs, and a random high-entropy secret, Presidio ships no recognizers and detects 0/9; SafeStream detects 9/9. This is a capability difference (Presidio has no credential recognizers at all), not a tuned accuracy comparison.

Development

git clone https://github.com/MounishSenisetty/SafeStream-Redactor
cd SafeStream-Redactor
pip install -e '.[dev]'
pytest                                   # fast suite (incl. property-based tests)
pytest --cov=safestream_redactor         # with the 95% coverage gate
SAFESTREAM_MEMTEST_MB=100 pytest -m memory -o addopts=''   # constant-memory proof
ruff check . && ruff format --check .       # lint + format
mypy                                        # strict type check

See CONTRIBUTING.md. Good first issues live in docs/good_first_issues.md and the issue tracker. The audited state of the codebase and the phased improvement plan are in docs/AUDIT.md and docs/ROADMAP.md; the security policy and threat model (what the offline guard does and does not cover) are in SECURITY.md.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

safestream_redactor-0.1.0.tar.gz (133.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

safestream_redactor-0.1.0-py3-none-any.whl (48.8 kB view details)

Uploaded Python 3

File details

Details for the file safestream_redactor-0.1.0.tar.gz.

File metadata

  • Download URL: safestream_redactor-0.1.0.tar.gz
  • Upload date:
  • Size: 133.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for safestream_redactor-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ad03de254bc503cccf8aada17e126ab667b8b897cb75b7192060441f071b7463
MD5 770bc468cedd19abe250d874783bfbf0
BLAKE2b-256 428e52cb9446436d45f74505b58383e18bac462ea654a7242f85b348e10aeb37

See more details on using hashes here.

Provenance

The following attestation bundles were made for safestream_redactor-0.1.0.tar.gz:

Publisher: publish.yml on MounishSenisetty/SafeStream-Redactor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file safestream_redactor-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for safestream_redactor-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cae5e0a94e03b375fce532c6cb3ca012457a1005b607b4a610d9f317d2f86117
MD5 da3f8cc02f86f767c401717b3d354a6e
BLAKE2b-256 89374593bfaef7632d8df09834feb1efb5cbd6e6ddd555ca08d066ad72a2e3c2

See more details on using hashes here.

Provenance

The following attestation bundles were made for safestream_redactor-0.1.0-py3-none-any.whl:

Publisher: publish.yml on MounishSenisetty/SafeStream-Redactor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page