|
1 | | - |
2 | | -# Scrub, the local-first security-artefact sanitiser |
| 1 | +# Scrub, local-first security-artefact sanitiser |
3 | 2 |
|
4 | | -Strip infrastructure identifiers and secrets out of logs, configs, and terminal output |
5 | | -**before** you paste them into a public GitHub issue, a vendor ticket, a forum, or a |
6 | | -blog post, and then **review what was detected and decide what to scrub** before you copy |
7 | | -it out. |
| 3 | +**Strip secrets and infrastructure identifiers out of logs, configs, and terminal |
| 4 | +output before you share them, with a human review step so you decide what leaves |
| 5 | +your machine.** |
8 | 6 |
|
9 | | -> **The data never leaves your machine.** That's the whole pitch, not a preference. |
10 | | -> There are no network calls in the scrubbing path, no telemetry, no phone-home. The API |
11 | | -> runs on `127.0.0.1`; the UI's browser talks only to that local API. |
| 7 | +Scrub runs entirely on your machine. Paste or pipe in an artefact; it detects |
| 8 | +internal IPs, hostnames, cloud keys, tokens, private keys, and connection strings, |
| 9 | +replaces each with a stable placeholder (e.g. `<INTERNAL_IP_1>`), and gives you a |
| 10 | +diff to review before anything is exported. Nothing is ever sent anywhere. |
12 | 11 |
|
13 | | -Scrub is not a general PII tool. It targets what actually leaks in security work, |
14 | | -internal IPs, hostnames, MACs, cloud keys, tokens, private keys, connection strings, |
15 | | -and adds the piece general tools don't: **consistent, reversible pseudonymisation** |
16 | | -(`<INTERNAL_IP_1>`), so the scrubbed artefact stays analytically useful and can be |
17 | | -restored byte-for-byte. |
| 12 | +--- |
18 | 13 |
|
19 | | -## Human-in-the-loop, by design |
| 14 | +## Why |
20 | 15 |
|
21 | | -Detection is **recall-favoured**, it surfaces everything, because under-scrubbing leaks |
22 | | -a secret. That's only safe if a human can see and correct what was flagged. So Scrub |
23 | | -**surfaces and applies; you decide**. The UI shows every detection, lets you toggle each |
24 | | -one, and **never claims the output is "safe" or "clean"**. Always eyeball the result |
25 | | -before sharing. |
| 16 | +Engineers leak secrets every day by pasting logs into GitHub issues, vendor |
| 17 | +tickets, forums, and chat. General PII tools target names and emails; they miss |
| 18 | +the things that actually leak in security and infrastructure work: internal IPs, |
| 19 | +MAC addresses, cloud keys, JWTs, private-key blocks, DB connection strings. |
26 | 20 |
|
27 | | -## Three ways to use it |
| 21 | +Scrub fills that gap, and keeps a reversible mapping so the scrubbed artefact |
| 22 | +stays useful (you can follow which host talked to which) without exposing the real |
| 23 | +identifiers. |
28 | 24 |
|
29 | | -| Dir | What | Stack | |
30 | | -|-----|------|-------| |
31 | | -| [`scrub/`](scrub) | The engine: detectors + consistent pseudonymiser + FastAPI service (`/scrub`, `/restore`, `/entities`, `/health`) **and the `scrub` CLI** | Python, Presidio | |
32 | | -| [`ui/`](ui) | The review front end: two-pane paste → review → export, with per-detection toggles | Next.js, TypeScript, Tailwind | |
33 | | -| [`hooks/`](hooks) | A git **pre-commit hook** that blocks commits containing secrets (built on `scrub --check`) | bash | |
| 25 | +--- |
34 | 26 |
|
35 | | -## Quick start (run both) |
| 27 | +## What it detects |
| 28 | + |
| 29 | +The detection pack ships **33 entity types**. Two are validated in code rather than |
| 30 | +trusted from a regex, IP addresses via the stdlib `ipaddress` module, and credit |
| 31 | +cards via the Luhn checksum, and several FP-prone patterns are deliberately scored |
| 32 | +low and only cross the threshold when supporting context words are nearby. |
| 33 | + |
| 34 | +- **Network identifiers:** `INTERNAL_IP`, `PUBLIC_IP` (IPv4 + IPv6, validated), |
| 35 | + `MAC_ADDRESS`, `HOSTNAME` |
| 36 | +- **Cloud / provider keys:** `AWS_ACCESS_KEY`, `AWS_SECRET_KEY`, `AWS_ACCOUNT_ID`, |
| 37 | + `GOOGLE_API_KEY`, `STRIPE_KEY`, `OPENAI_KEY`, `OPENROUTER_KEY`, |
| 38 | + `FCM_SERVER_KEY`, `SENDGRID_KEY`, `TWILIO_SID` |
| 39 | +- **Source-control / package tokens:** `GITHUB_TOKEN`, `GITLAB_TOKEN`, |
| 40 | + `NPM_TOKEN`, `SHOPIFY_TOKEN` |
| 41 | +- **Chat tokens & webhooks:** `SLACK_TOKEN`, `SLACK_WEBHOOK`, `DISCORD_TOKEN`, |
| 42 | + `DISCORD_WEBHOOK`, `TELEGRAM_BOT_TOKEN` |
| 43 | +- **Auth tokens / generic secrets:** `JWT`, `BEARER_TOKEN`, `GENERIC_API_KEY` |
| 44 | +- **Crypto / connection strings:** `PRIVATE_KEY_BLOCK` (PEM), `SSH_PUBLIC_KEY`, |
| 45 | + `DB_CONNECTION_STRING`, `URL_WITH_CREDENTIALS` |
| 46 | +- **PII:** `EMAIL_ADDRESS`, `CREDIT_CARD` (Luhn-validated) |
| 47 | +- **Filesystem:** `UNIX_HOME_PATH` (username leak) |
| 48 | + |
| 49 | +The live list is always available from the API (`GET /entities`) or |
| 50 | +`Scrubber().entities()`. You can also add your own regex recognisers at runtime |
| 51 | +from the review UI (see below), they run alongside the built-in pack. |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +## Install |
36 | 56 |
|
37 | 57 | ```bash |
38 | | -# terminal 1 – the API (no spaCy model needed for the default mode) |
39 | | -cd scrub |
40 | | -python -m venv .venv && . .venv/Scripts/activate # or: source .venv/bin/activate |
41 | | -pip install -r requirements.txt |
42 | | -uvicorn main:app --host 127.0.0.1 --port 8000 |
| 58 | +pip install -e . |
| 59 | +``` |
43 | 60 |
|
44 | | -# terminal 2 – the UI |
45 | | -cd ui |
46 | | -npm install |
47 | | -npm run dev # open http://localhost:3000 |
| 61 | +This puts a `scrub` command on your PATH. The only runtime dependency for the CLI |
| 62 | +is `presidio-analyzer`; the API service and full-Presidio mode are optional extras |
| 63 | +(`pip install -e ".[api]"`, and a spaCy model for full mode). |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## CLI usage |
| 68 | + |
| 69 | +```bash |
| 70 | +scrub app.log # scrubbed text -> stdout |
| 71 | +scrub app.log --mapping m.json # also save the reversible mapping |
| 72 | +cat app.log | scrub # read stdin, scrub, write stdout |
| 73 | +scrub --restore m.json s.txt # reconstruct the original |
| 74 | +scrub --check app.log # report secrets (stderr); exit 1 if any |
48 | 75 | ``` |
49 | 76 |
|
50 | | -Paste an artefact (try [`scrub/sample.log`](scrub/sample.log)), hit **Scrub**, review the |
51 | | -colour-coded detections, toggle anything you want to keep in the clear, and copy the |
52 | | -result. See [`scrub/README.md`](scrub/README.md) and [`ui/README.md`](ui/README.md) for |
53 | | -details, the optional full-Presidio mode, and tests. |
| 77 | +| Flag | Mode | Meaning | |
| 78 | +| --- | --- | --- | |
| 79 | +| `FILE…` | all | Input file(s). Omit or use `-` to read stdin. Multiple files are only meaningful with `--check`. | |
| 80 | +| `--check` | check | Detection only: print a masked report to stderr, exit 1 if any finding is at/above the threshold, else 0. Built for pre-commit hooks. | |
| 81 | +| `--restore MAPPING.json` | restore | Reconstruct the original from scrubbed input + this mapping. | |
| 82 | +| `--mapping PATH` | transform | Also write the reversible `{placeholder: original}` mapping as JSON to `PATH`. | |
| 83 | +| `-t`, `--threshold N` | transform / check | Only act on detections scoring ≥ N (default `0.6`; use `0` to scrub everything, recall over precision). | |
| 84 | +| `--allow ENTITY_TYPE` | check | Suppress an entity type, e.g. `--allow PUBLIC_IP`. Repeatable. | |
| 85 | +| `--label NAME` | check | Name to use for stdin in the report (default `<stdin>`). | |
| 86 | +| `--no-near-miss` | check | Don't print the non-blocking notice about sub-threshold detections on a passing commit. | |
| 87 | +| `--no-color` | all | Disable coloured output (colour is stderr-only and already auto-off when stderr isn't a TTY or `NO_COLOR` is set). | |
| 88 | +| `--quiet` | all | Suppress the progress spinner on slow interactive runs. | |
| 89 | +| `-V`, `--version` | n/a | Print version and exit. | |
| 90 | + |
| 91 | +Running `scrub` with no input and an interactive terminal prints usage and exits 2 |
| 92 | +(it won't hang waiting on stdin). |
| 93 | + |
| 94 | +The contract that keeps it pipeable and safe: |
| 95 | + |
| 96 | +- Scrubbed / restored text is the **only** thing on **stdout**. |
| 97 | +- Diagnostics, reports, and errors go to **stderr**, including a masked summary of |
| 98 | + low-confidence detections that were *not* applied, so nothing is silently dropped. |
| 99 | +- The reversible mapping holds the real secrets, so it is written **only** to the |
| 100 | + `--mapping` path you ask for, never to stdout. |
| 101 | +- `--check` and every report **mask** each value, so secrets never hit your |
| 102 | + scrollback or CI logs. |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Git pre-commit hook |
| 107 | + |
| 108 | +`--check` is designed to drop into a git pre-commit hook so secrets are caught |
| 109 | +before they are committed. A ready-made hook and installers live in |
| 110 | +[`hooks/`](hooks/): |
| 111 | + |
| 112 | +```bash |
| 113 | +hooks/install.sh # bash / macOS / Linux |
| 114 | +hooks/install.ps1 # Windows PowerShell |
| 115 | +``` |
54 | 116 |
|
55 | | -## Command line + git pre-commit hook |
| 117 | +The hook blocks a commit (exit 1) on any high-confidence finding and prints a |
| 118 | +masked report; sub-threshold near-misses are noted without blocking. |
56 | 119 |
|
57 | | -Install the CLI (puts `scrub` on PATH): |
| 120 | +--- |
| 121 | + |
| 122 | +## The review UI |
| 123 | + |
| 124 | +A local web UI (Next.js) for the paste → review → export flow: |
58 | 125 |
|
59 | 126 | ```bash |
60 | | -pip install -e . # from the repo root |
| 127 | +cd ui |
| 128 | +npm install |
| 129 | +npm run dev # http://localhost:3000 |
61 | 130 | ``` |
62 | 131 |
|
| 132 | +It talks only to the local API (`http://127.0.0.1:8000`); start that with: |
| 133 | + |
63 | 134 | ```bash |
64 | | -scrub app.log # scrubbed text -> stdout |
65 | | -cat app.log | scrub # pipeable |
66 | | -scrub app.log --mapping m.json # also save the reversible mapping (holds secrets) |
67 | | -scrub --restore m.json scrubbed.txt # reconstruct the original, byte-for-byte |
68 | | -scrub --check app.log # masked report -> stderr; exit 1 if secrets found |
| 135 | +cd scrub |
| 136 | +uvicorn main:app --host 127.0.0.1 --port 8000 |
69 | 137 | ``` |
70 | 138 |
|
71 | | -**Block secrets at commit time.** The `scrub --check` mode exits non-zero when it finds |
72 | | -a secret, which lets a git pre-commit hook abort the commit: |
| 139 | +What the UI gives you: |
| 140 | + |
| 141 | +- **Paste or load files:** drag-and-drop or pick multiple files; each opens in its |
| 142 | + own tab. Files are read in the browser; only the text reaches the localhost API. |
| 143 | +- **Review with line numbers:** the scrubbed output is line-numbered; click a |
| 144 | + detection to scroll to and highlight that line. A gutter "minimap" shows where in |
| 145 | + the artefact data was scrubbed. |
| 146 | +- **Diff view:** toggle to see the original (with highlights) beside the scrubbed |
| 147 | + output. |
| 148 | +- **Per-detection control:** keep/dismiss individual detections, filter/search the |
| 149 | + list, and move a confidence-threshold slider; the export updates instantly. |
| 150 | +- **Custom recognisers:** add your own regex patterns at runtime (in memory only). |
| 151 | +- **Restore round-trip:** reconstruct the original from the in-memory mapping to |
| 152 | + confirm the scrub is reversible. |
| 153 | + |
| 154 | +API endpoints (all localhost-only): `POST /scrub`, `POST /restore`, |
| 155 | +`GET /entities`, `GET /health`, and `GET/POST/DELETE /recognizers` for the custom |
| 156 | +recogniser set. |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## How it works |
| 161 | + |
| 162 | +- **Stable, reversible pseudonymisation.** Every occurrence of the same value gets |
| 163 | + the same placeholder, so the scrubbed artefact still reads as a coherent trace; |
| 164 | + the kept mapping reconstructs the original byte-for-byte. |
| 165 | +- **Context-aware scoring.** In the default (custom-pack-only, no-spaCy) mode, a |
| 166 | + lightweight character-window check raises a detection's score when the |
| 167 | + recogniser's context words are nearby, so a real AWS secret next to |
| 168 | + `aws_secret_access_key` clears the threshold while a bare 40-char blob does not. |
| 169 | +- **Validated, not just matched.** IPs and credit-card numbers are validated in |
| 170 | + code (stdlib `ipaddress`, Luhn) rather than trusted from a regex. |
| 171 | +- **Local-first.** No network calls in the scrub path; no telemetry. |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +## Optional: full Presidio mode |
| 176 | + |
| 177 | +The default mode runs the custom pack only, no spaCy model needed. To also get |
| 178 | +Presidio's built-in human-PII recognisers (`PERSON`, etc.) on top: |
73 | 179 |
|
74 | 180 | ```bash |
75 | | -./hooks/install.sh # copies hooks/pre-commit into this repo's .git/hooks |
| 181 | +python -m spacy download en_core_web_lg |
76 | 182 | ``` |
77 | 183 |
|
78 | | -…or, for [pre-commit](https://pre-commit.com) framework users, the repo ships a |
79 | | -[`.pre-commit-config.yaml`](.pre-commit-config.yaml) (`pre-commit install`). Reports are |
80 | | -**masked** so secrets never hit your scrollback, and the hook is an honest safety net which is |
81 | | -bypassable with `git commit --no-verify`. Full CLI + hook docs in |
82 | | -[`scrub/README.md`](scrub/README.md). |
| 184 | +```python |
| 185 | +Scrubber(use_nlp_engine=True) # security pack + built-in PII |
| 186 | +``` |
83 | 187 |
|
84 | | -## Security & CI |
| 188 | +For the API, set `SCRUB_USE_NLP=1` before launching `uvicorn`. If the model isn't |
| 189 | +installed, Scrub fails fast with the exact `spacy download` command, it never |
| 190 | +silently degrades. |
| 191 | + |
| 192 | +--- |
| 193 | + |
| 194 | +## Architecture |
| 195 | + |
| 196 | +- **`security_recognizers.py`:** the detection pack (Presidio recognisers + |
| 197 | + validated IP/credit-card logic). |
| 198 | +- **`pseudonymizer.py`:** turns detections into stable placeholders and back. |
| 199 | +- **`scrubber.py`:** ties detection + pseudonymisation behind one `Scrubber` |
| 200 | + class, and holds any runtime custom recognisers. |
| 201 | +- **`context_enhancer.py`:** spaCy-free context scoring for custom-only mode. |
| 202 | +- **`cli.py`:** the command-line interface. |
| 203 | +- **`main.py`:** the localhost API used by the review UI. |
| 204 | +- **`ui/`:** the Next.js review UI. |
| 205 | + |
| 206 | +--- |
| 207 | + |
| 208 | +## Known limitations |
| 209 | + |
| 210 | +- Detection is **pattern + context based**, not semantic. Novel, obfuscated, or |
| 211 | + malformed secrets may not match, by design, the pack favours precision on the |
| 212 | + high-confidence patterns and leans on context for the ambiguous ones. |
| 213 | +- At low thresholds, bare values can be mislabelled (the long tail favours recall); |
| 214 | + raise `--threshold`, or use the review step, to tighten this. |
| 215 | +- The pack is **not exhaustive:** it targets the identifiers that actually leak in |
| 216 | + security/infra work, not every possible secret format. |
| 217 | +- **Review before export is the safety net.** Scrub surfaces what it found (and what |
| 218 | + it nearly found); a human still decides what is safe to share. |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## Testing |
| 223 | + |
| 224 | +```bash |
| 225 | +cd scrub && pytest |
| 226 | +``` |
85 | 227 |
|
86 | | -Dependency advisories are triaged and documented in [`SECURITY.md`](SECURITY.md) (a |
87 | | -security tool shouldn't ship untriaged vulns), and [`.github/workflows/ci.yml`](.github/workflows/ci.yml) |
88 | | -runs the Python tests, the UI build/type-check, and a dependency audit gated at `high` on |
89 | | -every push and PR. |
| 228 | +--- |
90 | 229 |
|
91 | | -## No persistence of secrets |
| 230 | +## License |
92 | 231 |
|
93 | | -The mapping that reverses a scrub holds the real secrets. It is returned by the API for |
94 | | -the caller to hold and is kept **in memory only** in the UI. It is never written to disk, |
95 | | -`localStorage`, or anywhere else. The repo's `.gitignore` also excludes |
96 | | -`*.mapping.json`. Treat any mapping you do save like the credentials it contains. |
| 232 | +MIT |
97 | 233 |
|
98 | | -## Roadmap (not built yet) |
| 234 | +--- |
99 | 235 |
|
100 | | -Format-preserving pseudonymisation (fake-but-valid IP/MAC), an encrypted vault for the |
101 | | -mapping at rest, and screenshot OCR + visual redaction. Any future LLM recogniser must be |
102 | | -a **local** model (e.g. Ollama) and opt-in, the local-first promise is non-negotiable. |
| 236 | +*This README reflects scrub v0.3.0. The detected-entity list and CLI flags are the |
| 237 | +two things most likely to drift; keep this section honest as recognisers are added.* |
0 commit comments