Skip to content

louievandyke/sanitize-cli

Repository files navigation

sanitize — Local Support-Text Sanitizer

A small, local-first CLI that strips secrets and customer-identifying data out of support notes, logs, configs (HCL/JSON/YAML/.env), and general text — so the result is safer to paste into AI tools like Cursor, ChatGPT, Codex, Claude Code, or other agent-based coding assistants.

It is built around one idea: don't blindly replace everything with <REDACTED>. Sensitive values are swapped for stable, typed placeholders (<HOST_1>, <PRIVATE_IP_2>, <REDACTED_VAULT_TOKEN_1>), so you keep the topology, relationships, and debugging context while losing the sensitive bits.

10.42.1.15 failed to connect to 10.42.1.16 on port 4647
        ↓
<PRIVATE_IP_1> failed to connect to <PRIVATE_IP_2> on port 4647

What it does NOT guarantee

This tool reduces risk but does not guarantee that text is safe to share externally. Always review sanitized output before pasting into third-party tools.

It is a regex/heuristic tool, not a security product. It can miss novel secret formats and can occasionally over-redact. Treat it as a first pass, not a guarantee.

Install

Requires Python 3.11+. No third-party runtime dependencies.

# from the project root
python3 -m venv .venv
source .venv/bin/activate
pip install -e .          # provides the `sanitize` command
# (for tests) pip install -e ".[dev]"

You can also run it without installing:

python -m sanitizer.cli input.txt

CLI usage

sanitize input.txt                       # sanitized text to stdout
sanitize input.txt --output sanitized.txt
sanitize input.txt --report              # count summary to stderr
sanitize input.txt --json-report         # machine-readable summary to stderr
sanitize input.txt --profile strict
sanitize input.txt --profile infra-safe
sanitize input.txt --profile case-summary
sanitize input.txt --customer "Acme Corp" --customer "Globex"
cat input.txt | sanitize                 # read from stdin

Reports go to stderr, sanitized text to stdout, so redirection stays clean: sanitize notes.txt --report > clean.txt writes only the sanitized text to the file and prints the report to your terminal.

Flag Purpose
-o, --output FILE Write sanitized text to a file instead of stdout.
--profile NAME strict, infra-safe (default), or case-summary.
--report Print a count summary to stderr.
--json-report Print a JSON summary to stderr.
--customer NAME Literal org/customer name to redact as <ORG_n> (repeatable).
--allow TERM Term to never treat as a hostname (repeatable).
--config FILE Path to a .sanitizer.yml config (overrides auto-discovery).
--no-config Skip auto-discovery of a .sanitizer.yml file.
--debug Include original values in the report (local use only; off by default).

Profiles

Profile Use it when… Redacts Preserves
strict You're unsure the content is safe. Secrets, tokens, emails, URLs, IPs, hostnames, orgs, UUIDs, phone numbers. Ports, timestamps, error text.
infra-safe You want infra/debugging context intact. Secrets, tokens, creds, emails, URLs, hostnames, customer names, IPs (as stable placeholders). Timestamps, ports, protocols, status codes, error messages, cloud regions, generic service names.
case-summary Pasting a case narrative into an AI assistant. Customer identity, emails, hostnames, URLs, tokens, secrets, phone numbers. Product names (Nomad, Consul, Vault, Terraform…), versions, ports, the technical narrative.

Before / after

Input:

Customer Acme Corp reported failures from nomad-client-22.prod.acme.internal.
Reporter: jane.doe@acme.example.com
server at 10.42.1.15:4647 refused; egress to 35.91.12.8:443 throttled.
vault token hvs.CAESIJ1example0token0value0only0fake00000
AWS_ACCESS_KEY_ID=AKIA1234567890ABCDEF

sanitize --profile infra-safe --customer "Acme Corp":

Customer <ORG_1> reported failures from <HOST_1>.
Reporter: <REDACTED_EMAIL_1>
server at <PRIVATE_IP_1>:4647 refused; egress to <PUBLIC_IP_1>:443 throttled.
vault token <REDACTED_VAULT_TOKEN_1>
AWS_ACCESS_KEY_ID=<REDACTED_AWS_ACCESS_KEY_1>

Run it yourself against the bundled samples:

sanitize examples/case-notes.txt --profile infra-safe --customer "Acme Corp" --report
sanitize examples/nomad-client.log --profile infra-safe --report
sanitize examples/vault-config.hcl --report

What gets detected

  • Secrets/tokens: Vault (hvs./hvb./s.), GitHub, Slack, JWTs, Bearer tokens, and generic password=/token=/secret=/client_secret= assignments.
  • Cloud credentials: AWS access keys (AKIA/ASIA), labeled AWS secret keys, and PEM private-key blocks (redacted whole).
  • HashiCorp Nomad/Consul: ACL tokens (NOMAD_TOKEN/CONSUL_HTTP_TOKEN env vars and Consul tokens { agent = … } block roles), Serf gossip encryption keys (the encrypt field), and cluster-identifying resource IDs (alloc_id, node_id, eval_id, deployment_id) which become readable, correlated placeholders like <ALLOC_ID_1>. Try examples/consul-agent.hcl and examples/nomad-scheduler.log.
  • Identity: emails, phone numbers, URLs, customer/org names (via --customer).
  • Network: private vs public IPv4/IPv6 (classified with the ipaddress module), CIDR ranges (prefix preserved), hostnames/FQDNs.

Placeholders are deterministic within a run: the same original value always maps to the same placeholder, so repeated hosts/IPs stay correlated.

Config file (.sanitizer.yml)

Drop a .sanitizer.yml in your repo root (or anywhere up the tree from the input file) and it's picked up automatically; or point at one with --config path/to/.sanitizer.yml. Use --no-config to ignore it.

customer_names:        # redacted as <ORG_n>
  - Acme Corp
  - Globex

allowlist:             # never treated as hostnames
  - my-special-01

extra_patterns:        # custom detectors; `label` is optional
  - name: INTERNAL_CASE_ID
    regex: "CASE-[0-9]+"
    label: CASE_ID       # -> <CASE_ID_n>  (defaults to REDACTED_<NAME>)

Config values are merged with --customer / --allow flags. To stay dependency-free the loader parses the small YAML subset shown above (top-level keys, - scalar lists, and - key: value mapping lists); see examples/sanitizer.yml.example. Anything more elaborate isn't supported.

How to add a new pattern

  1. Open sanitizer/patterns.py and add a Detector:

    INTERNAL_CASE_ID = Detector(
        name="INTERNAL_CASE_ID",            # shows up in --report
        pattern=re.compile(r"\bCASE-[0-9]+\b"),
        label="REDACTED_CASE_ID",           # output is <REDACTED_CASE_ID_n>
    )

    To redact only part of a match (and keep a surrounding label), set group= to the capture-group index of the value.

  2. Add it to REGEX_DETECTORS in the right order (more specific patterns first).

  3. Enable it for the relevant profiles in sanitizer/profiles.py.

  4. Add a test under tests/.

Detectors that need real logic (IP classification, hostname heuristics) live in sanitizer/engine.py.

Running tests

pip install -e ".[dev]"   # or: pip install pytest
pytest

Security notes

  • Runs entirely locally — input is never sent to any external service. The tool has no network code and no third-party runtime dependencies (standard library only).
  • Raw input is never logged; original values are kept out of reports and out of Finding reprs by default. --debug opts in to showing originals locally.
  • Every value in examples/ and tests/ is fake and non-identifying — invented tokens, RFC 1918 / documentation IPs, and fictional names (Acme Corp, Globex) and domains (*.example.com, *.acme.internal).
  • You must still review the output before sharing it externally.

License

Released under the MIT License.

About

Local-first CLI and Agent Skill for sanitizing logs, configs, and support notes before pasting into AI tools.

Topics

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages