privatiser 0.1.0
Content anonymizer/pseudonymizer — redact sensitive data before sharing with AI
Requires Python: >=3.11
Keywords: anonymize, privacy, redact, pii, secrets, ai

# Privatiser

Anonymize sensitive content before sharing with AI. Redacts IPs, emails, API keys, passwords, and more — replacing them with structurally valid pseudonyms so AI analysis remains accurate. Fully reversible.

**Everything runs locally. Nothing leaves your machine.**

Available as a **Python CLI/library**, a **web UI**, and a **browser extension** for Chrome and Firefox.

## What it detects

| Category | Examples | Pseudonym format |
|---|---|---|
| IP addresses | `192.168.1.100`, `10.0.0.0/16` | `10.x.x.x` (preserves CIDR) |
| Email addresses | `admin@company.com` | `user-1@redacted.example.net` |
| Domain names | `prod-db.mycompany.com` | `redacted-host-1.example.net` |
| MAC addresses | `AA:BB:CC:DD:EE:FF` | `AA:BB:CC:00:00:01` |
| AWS Account IDs | `123456789012` | `100000000001` |
| AWS ARNs | `arn:aws:iam::123...:role/admin` | Structure preserved, values redacted |
| S3 buckets | `s3://my-prod-bucket` | `s3://redacted-bucket-1` |
| API keys | AWS, OpenAI, Anthropic, Google, Groq, GitHub, Slack | `REDACTED_SECRET_n` |
| Connection strings | `postgresql://user:pass@host/db` | `REDACTED_CONNSTR_n` |
| JWT tokens | `eyJhbG...` | `REDACTED_JWT_n` |
| PEM private keys | `-----BEGIN RSA PRIVATE KEY-----` | `REDACTED_PEM_KEY_n` |
| Generic secrets | `password = "value"` | Keyword preserved, value redacted |
| US phone numbers | `(555) 123-4567`, `+1-555-123-4567` | `(555) 000-0001` |
| UK phone numbers | `+44 7911 123456` | `+44 7700 900001` |
| Credit cards | `4111 1111 1111 1111` (Luhn validated) | `4000-0000-0000-0001` |
| US SSN | `123-45-6789` | `078-05-0001` |
| Passports | `C12345678` | `X00000001` |
| IBAN | `DE89370400440532013000` | `GB00XXXX000000000001` |
| UUIDs | `550e8400-e29b-41d4-...` | `00000000-0000-4000-a000-...` |

Skips well-known values: `127.0.0.1`, `0.0.0.0`, cloud provider domains (`amazonaws.com`, `github.com`, etc.).

## Installation

### Python package

```bash
git clone https://github.com/yourusername/privatiser.git
cd privatiser

python3 -m venv .venv
source .venv/bin/activate

# Install with all extras
pip install -e ".[dev,web]"
```

### Browser extension

The extension source lives in `extension/`. A build script generates platform-specific versions for Chrome and Firefox.

```bash
# Build both Chrome and Firefox versions
./build-extension.sh
```

This creates:
- `dist-ext/chrome/` — Chrome extension (uses `service_worker`, subdirectory structure)
- `dist-ext/firefox/` — Firefox extension (uses `scripts`, flat structure)
- `dist-ext/privatiser-chrome.zip` — Chrome distribution package
- `dist-ext/privatiser-firefox.zip` — Firefox distribution package

**Load in Chrome:**
1. Go to `chrome://extensions`
2. Enable **Developer mode** (top right)
3. Click **Load unpacked** and select the `dist-ext/chrome/` folder

**Load in Firefox:**
```bash
cd dist-ext/firefox && web-ext run
```

> Firefox on macOS has issues loading temporary add-ons via `about:debugging` due to file access restrictions. Using [web-ext](https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/) is the recommended approach.

Install web-ext if you don't have it:
```bash
npm install -g web-ext
```

## Usage

### CLI

```bash
# Anonymize from stdin
cat config.tf | privatiser anonymize

# Anonymize a file, save mapping
privatiser anonymize config.tf -o clean.tf -m mapping.json

# Anonymize from clipboard
privatiser anonymize -c -m mapping.json

# Restore original content
privatiser deanonymize clean.tf -m mapping.json

# Launch web UI
privatiser ui
privatiser ui --port 8080 --debug
```

### Python API

```python
from privatiser import Privatiser

p = Privatiser()

# Anonymize
text = 'server = "192.168.1.100"\npassword = "secret123"'
anonymized, mapping = p.anonymize(text)
print(anonymized)
# server = "10.0.1.8"
# password = "REDACTED_SECRET_1"

# Restore
restored = p.deanonymize(anonymized, mapping)
assert restored == text  # Perfect round-trip

# Works on nested data structures too
data = {"servers": [{"ip": "10.0.1.8"}]}
restored_data = p.deanonymize_data(data, mapping)
```

### Custom patterns

```python
from privatiser import Privatiser, register_custom

register_custom("ticket_id", r"TICKET-\d{4,6}", "REDACTED_TICKET_{n}")

p = Privatiser()
result, mapping = p.anonymize("Fix TICKET-12345")
# result: "Fix REDACTED_TICKET_1"
```

### Web UI

```bash
privatiser ui
```

Opens at `http://127.0.0.1:7860`. Two tabs:

- **Anonymize** — Paste text, click Anonymize, copy the safe output. Mapping table shows what was redacted.
- **Deanonymize** — Paste anonymized text + mapping JSON, restore originals.

Dark/light theme. Keyboard shortcut: `Ctrl+Enter` to process.

### Browser extension

The extension works in three ways:

#### 1. Popup (manual mode)

Click the Privatiser icon in your toolbar. Three tabs:

- **Anonymize** — Paste sensitive text, click Anonymize, copy the cleaned output. A mapping table shows every redaction.
- **Restore** — Paste anonymized text + the mapping JSON to get back the originals.
- **Settings** — Toggle auto-intercept, badge counts, and mapping persistence.

#### 2. Auto-intercept (paste protection)

When enabled (on by default), the extension automatically anonymizes anything you paste into supported AI chat sites:

- **ChatGPT** (chatgpt.com / chat.openai.com)
- **Claude** (claude.ai)
- **Gemini** (gemini.google.com)
- **Copilot** (copilot.microsoft.com)

**How it works:**
1. Copy text containing sensitive data (configs, logs, code, etc.)
2. Paste into any of the above AI chat sites
3. The extension intercepts the paste, anonymizes the content, and inserts the cleaned version
4. A toast notification shows how many items were redacted
5. The toolbar icon badge shows the redaction count
6. The mapping is stored in session memory — use the Restore tab to get originals back

The AI receives the anonymized text and can still analyse it normally because pseudonyms are structurally valid (IPs look like IPs, emails look like emails, etc.). When the AI responds, use the mapping to restore real values in the output.

#### 3. Right-click menu

Select any text on any page, right-click, and choose **"Anonymize with Privatiser"** or **"Anonymize & Copy to Clipboard"**.

### Typical AI workflow

```
┌─────────────┐    paste    ┌──────────────┐    send    ┌─────────┐
│ Your config │ ──────────► │  Extension   │ ────────► │  AI Chat │
│ with real   │             │  auto-redacts │           │  sees    │
│ IPs, keys   │             │  on paste     │           │  safe    │
│ passwords   │             │               │           │  pseudos │
└─────────────┘             └──────┬───────┘           └────┬────┘
                                   │                        │
                            mapping saved              AI responds
                            in session                 with pseudos
                                   │                        │
                                   ▼                        ▼
                            ┌──────────────┐    restore  ┌─────────┐
                            │ Restore tab  │ ◄────────── │  Copy   │
                            │ + mapping    │             │  AI     │
                            │ = originals  │             │  output │
                            └──────────────┘             └─────────┘
```

**Example:**

You want to ask ChatGPT to help debug a database connection issue. Your config contains:
```
host: prod-db.acmecorp.com
port: 5432
user: admin
password: SuperS3cret!
connection: postgresql://admin:SuperS3cret!@prod-db.acmecorp.com:5432/maindb
```

With auto-intercept on, you paste this into ChatGPT. The AI actually receives:
```
host: redacted-host-1.example.net
port: 5432
user: admin
password: REDACTED_SECRET_1
connection: REDACTED_CONNSTR_1
```

ChatGPT can still diagnose the issue (wrong port, missing SSL, etc.) — it just never sees your real credentials or hostnames. When you get the answer back, use the Restore tab to swap pseudonyms for real values.

## Architecture

```
privatiser/
├── src/privatiser/
│   ├── core.py              # Privatiser class — the anonymization engine
│   ├── patterns/            # Modular pattern handlers
│   │   ├── __init__.py      # PatternHandler dataclass + registry
│   │   ├── network.py       # IP, domain, email, MAC
│   │   ├── aws.py           # Account ID, ARN, S3
│   │   ├── secrets.py       # API keys, connection strings, JWT, PEM
│   │   ├── pii.py           # Phone, credit card, SSN, passport, IBAN
│   │   └── identifiers.py   # UUID, custom patterns
│   ├── cli.py               # Click CLI
│   └── web/                 # Flask web UI
├── extension/               # Browser extension source
│   ├── manifest.json        # Base manifest (edit here)
│   ├── src/
│   │   ├── privatiser.js    # JS port of the anonymization engine
│   │   ├── background.js    # Context menu, badge, storage
│   │   ├── content.js       # Paste interception on AI sites
│   │   └── content.css      # Toast notification styles
│   └── popup/               # Extension popup UI
├── build-extension.sh       # Generates Chrome + Firefox builds
├── dist-ext/                # Build output (gitignored)
│   ├── chrome/              # Chrome extension (service_worker)
│   └── firefox/             # Firefox extension (flat, scripts)
└── tests/                   # 78 tests covering all patterns
```

### How it works

1. **Pattern registry** — Each pattern type (IP, email, secret, etc.) is a `PatternHandler` dataclass with a regex, pseudonym generator, priority, and optional validator.
2. **Priority ordering** — More specific patterns (connection strings, ARNs) run first to prevent partial matches by less specific patterns (IPs, domains).
3. **Placeholder system** — During anonymization, matches are replaced with null-byte placeholders, preventing later patterns from re-matching pseudonyms from earlier patterns. Placeholders are swapped for actual pseudonyms at the end.
4. **Consistent mapping** — The same input value always maps to the same pseudonym within a session. `192.168.1.1` appearing 10 times will be the same pseudonym every time.
5. **Reversible** — The mapping `{pseudonym: original}` is returned and can be saved to JSON for later deanonymization.

### Why separate Chrome/Firefox builds?

Chrome and Firefox have different Manifest V3 requirements:

| Feature | Chrome | Firefox |
|---|---|---|
| Background scripts | `service_worker` (required) | `scripts` array (preferred) |
| Context menus permission | `contextMenus` | `menus` |
| File structure | Subdirectories fine | Flat structure works better on macOS |
| Session storage | `chrome.storage.session` | Supported (115+), wrapped with fallback |

The `build-extension.sh` script generates both from the shared `extension/` source.

## Development

```bash
# Run tests
pytest tests/ -v

# Run a specific test file
pytest tests/test_pii.py -v

# Quick test
pytest tests/ -q

# Rebuild extensions after changes
./build-extension.sh

# Test Firefox extension
cd dist-ext/firefox && web-ext run

# Lint Firefox extension
cd dist-ext/firefox && web-ext lint
```

## Distribution

### PyPI

```bash
pip install build twine
python -m build
twine upload dist/*
```

### Chrome Web Store

1. Run `./build-extension.sh`
2. Upload `dist-ext/privatiser-chrome.zip` at the [Chrome Developer Dashboard](https://chrome.google.com/webstore/devconsole)
3. Fill in listing details, screenshots, privacy policy
4. Submit for review (typically 1-3 business days)

### Firefox Add-ons

1. Run `./build-extension.sh`
2. Upload `dist-ext/privatiser-firefox.zip` at [Firefox Add-on Developer Hub](https://addons.mozilla.org/developers/)
3. The `browser_specific_settings.gecko.id` is already set in the manifest
4. Submit for review

## Security

- All processing happens locally — no network requests, no external services
- The browser extension only activates on specific AI chat domains
- Mapping data uses session storage (cleared when browser closes)
- No data is persisted unless you explicitly enable "Remember mappings" in settings
- Extension message passing validates sender origins

## License

MIT