pynonymizer-core 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pynonymizer_core-1.0.0/LICENSE +21 -0
- pynonymizer_core-1.0.0/PKG-INFO +156 -0
- pynonymizer_core-1.0.0/README.md +134 -0
- pynonymizer_core-1.0.0/pynonymizer_core.egg-info/PKG-INFO +156 -0
- pynonymizer_core-1.0.0/pynonymizer_core.egg-info/SOURCES.txt +13 -0
- pynonymizer_core-1.0.0/pynonymizer_core.egg-info/dependency_links.txt +1 -0
- pynonymizer_core-1.0.0/pynonymizer_core.egg-info/entry_points.txt +3 -0
- pynonymizer_core-1.0.0/pynonymizer_core.egg-info/top_level.txt +1 -0
- pynonymizer_core-1.0.0/pyproject.toml +40 -0
- pynonymizer_core-1.0.0/setup.cfg +4 -0
- pynonymizer_core-1.0.0/src/cli/main.py +56 -0
- pynonymizer_core-1.0.0/src/core/anonymizer.py +171 -0
- pynonymizer_core-1.0.0/src/core/generators.py +79 -0
- pynonymizer_core-1.0.0/src/mcp/server.py +159 -0
- pynonymizer_core-1.0.0/tests/test_pynonymizer.py +133 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 THE HIVE // BSO (Chris & tastenkasperle)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynonymizer-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Native Zero-Dependency In-Memory Database Anonymizer & GDPR Test Data Generator (CLI & MCP Server)
|
|
5
|
+
Author-email: THE HIVE // BSO <diamantenschmiede@proton.me>
|
|
6
|
+
Maintainer: tastenkasperle
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/tastenkasperle/pynonymizer_core
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/tastenkasperle/pynonymizer_core/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/tastenkasperle/pynonymizer_core.git
|
|
11
|
+
Keywords: mcp,mcp-server,database,sqlite,anonymizer,gdpr,dsgvo,test-data,zero-dependency
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# 🛡️ Pynonymizer Core
|
|
24
|
+
|
|
25
|
+
[](https://opensource.org/licenses/MIT)
|
|
26
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
27
|
+
[](https://modelcontextprotocol.io)
|
|
28
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
29
|
+
[](https://gdpr-info.eu/art-32-gdpr/)
|
|
30
|
+
|
|
31
|
+
**Pynonymizer Core** is a native, zero-dependency in-memory SQLite database anonymization engine and GDPR/DSGVO test data generator designed for developers, CI/CD pipelines, and autonomous AI agents.
|
|
32
|
+
|
|
33
|
+
Freed from brittle host dependencies (`pg_dump`, `mysql` in `$PATH`) and heavy ORM overhead, Pynonymizer Core operates directly via Python's native `sqlite3` runtime—either in-memory or on disk with safe copy-on-write isolation.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🌟 Key Features
|
|
38
|
+
|
|
39
|
+
- **Zero External Dependencies:** Built purely on Python's standard library. Instant portability across Windows, Linux, and macOS without binary compilation.
|
|
40
|
+
- **Auto Strategy Inference:** Intelligently detects PII columns (names, emails, phone numbers, addresses, salaries, tax IDs) and generates declarative masking strategies.
|
|
41
|
+
- **In-Memory & File-Safe Operations:** Execute high-throughput anonymization entirely in RAM or create verified, scrubbed file copies.
|
|
42
|
+
- **Model Context Protocol (MCP) Ready:** First-class Stdio JSON-RPC 2.0 interface for Claude Desktop, Cursor, Antigravity, and autonomous agent frameworks.
|
|
43
|
+
- **Raptor Guard V2 Certified:** Hardened against SQL injection, column identifier tampering, and path traversal vectors.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🚀 Quickstart & Installation
|
|
48
|
+
|
|
49
|
+
Install directly via pip:
|
|
50
|
+
```bash
|
|
51
|
+
pip install .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install from distribution:
|
|
55
|
+
```bash
|
|
56
|
+
pip install pynonymizer-core
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 💻 CLI Usage
|
|
62
|
+
|
|
63
|
+
The command-line interface provides lightweight, modular commands for inspection, rule generation, and execution.
|
|
64
|
+
|
|
65
|
+
### 1. Inspect Database Schema
|
|
66
|
+
Inspect tables, columns, data types, and primary keys:
|
|
67
|
+
```bash
|
|
68
|
+
pynonymizer-core inspect /path/to/database.db
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Auto-Generate Anonymization Strategy
|
|
72
|
+
Infer column semantics and export an editable strategy JSON file:
|
|
73
|
+
```bash
|
|
74
|
+
pynonymizer-core generate-strategy /path/to/database.db > strategy.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Example `strategy.json`:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"users": {
|
|
81
|
+
"email": "unique_email",
|
|
82
|
+
"name": "full_name",
|
|
83
|
+
"phone": "phone_number"
|
|
84
|
+
},
|
|
85
|
+
"employees": {
|
|
86
|
+
"salary": "random_int"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Execute Anonymization
|
|
92
|
+
Anonymize a database file into a sanitized target output:
|
|
93
|
+
```bash
|
|
94
|
+
pynonymizer-core anonymize /path/to/database.db --strategy strategy.json --output /path/to/anonymized.db
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🤖 Model Context Protocol (MCP) Server
|
|
100
|
+
|
|
101
|
+
Pynonymizer Core includes an embedded Stdio MCP server compliant with JSON-RPC 2.0, giving AI agents direct tools to inspect schemas and sanitize datasets on demand.
|
|
102
|
+
|
|
103
|
+
### Provided Tools
|
|
104
|
+
|
|
105
|
+
| Tool Name | Description | Required Arguments |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `db_inspect_schema` | Returns tables, columns, types, and primary keys | `db_path` |
|
|
108
|
+
| `db_generate_strategy` | Evaluates schema and generates recommended GDPR masking map | `db_path` |
|
|
109
|
+
| `db_anonymize` | Applies declarative strategy to sanitize database | `source_db`, `strategy` (optional: `target_db`) |
|
|
110
|
+
|
|
111
|
+
### Claude Desktop / Cursor Setup
|
|
112
|
+
|
|
113
|
+
Add the following snippet to your `claude_desktop_config.json`:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"pynonymizer-core": {
|
|
119
|
+
"command": "python",
|
|
120
|
+
"args": ["-m", "src.mcp.server"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or using Docker:
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"pynonymizer-core": {
|
|
131
|
+
"command": "docker",
|
|
132
|
+
"args": ["run", "-i", "--rm", "pynonymizer-core"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 🧪 Testing & Security Verification
|
|
141
|
+
|
|
142
|
+
Run the test suite:
|
|
143
|
+
```bash
|
|
144
|
+
python -m unittest discover -s tests
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Security Posture (Raptor Guard V2)
|
|
148
|
+
- **Zero Sinks / Leaks:** No outbound socket requests or analytics telemetry.
|
|
149
|
+
- **Prepared Statements:** Parameterized queries for deterministic execution.
|
|
150
|
+
- **Deterministic Tokenization:** Safe synthetic data replacement conforming to GDPR Art. 32 requirements.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📄 License
|
|
155
|
+
|
|
156
|
+
MIT License. Developed and maintained by **THE HIVE // BSO** ([diamantenschmiede](https://github.com/tastenkasperle)).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 🛡️ Pynonymizer Core
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
5
|
+
[](https://modelcontextprotocol.io)
|
|
6
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
7
|
+
[](https://gdpr-info.eu/art-32-gdpr/)
|
|
8
|
+
|
|
9
|
+
**Pynonymizer Core** is a native, zero-dependency in-memory SQLite database anonymization engine and GDPR/DSGVO test data generator designed for developers, CI/CD pipelines, and autonomous AI agents.
|
|
10
|
+
|
|
11
|
+
Freed from brittle host dependencies (`pg_dump`, `mysql` in `$PATH`) and heavy ORM overhead, Pynonymizer Core operates directly via Python's native `sqlite3` runtime—either in-memory or on disk with safe copy-on-write isolation.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🌟 Key Features
|
|
16
|
+
|
|
17
|
+
- **Zero External Dependencies:** Built purely on Python's standard library. Instant portability across Windows, Linux, and macOS without binary compilation.
|
|
18
|
+
- **Auto Strategy Inference:** Intelligently detects PII columns (names, emails, phone numbers, addresses, salaries, tax IDs) and generates declarative masking strategies.
|
|
19
|
+
- **In-Memory & File-Safe Operations:** Execute high-throughput anonymization entirely in RAM or create verified, scrubbed file copies.
|
|
20
|
+
- **Model Context Protocol (MCP) Ready:** First-class Stdio JSON-RPC 2.0 interface for Claude Desktop, Cursor, Antigravity, and autonomous agent frameworks.
|
|
21
|
+
- **Raptor Guard V2 Certified:** Hardened against SQL injection, column identifier tampering, and path traversal vectors.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 🚀 Quickstart & Installation
|
|
26
|
+
|
|
27
|
+
Install directly via pip:
|
|
28
|
+
```bash
|
|
29
|
+
pip install .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install from distribution:
|
|
33
|
+
```bash
|
|
34
|
+
pip install pynonymizer-core
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 💻 CLI Usage
|
|
40
|
+
|
|
41
|
+
The command-line interface provides lightweight, modular commands for inspection, rule generation, and execution.
|
|
42
|
+
|
|
43
|
+
### 1. Inspect Database Schema
|
|
44
|
+
Inspect tables, columns, data types, and primary keys:
|
|
45
|
+
```bash
|
|
46
|
+
pynonymizer-core inspect /path/to/database.db
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Auto-Generate Anonymization Strategy
|
|
50
|
+
Infer column semantics and export an editable strategy JSON file:
|
|
51
|
+
```bash
|
|
52
|
+
pynonymizer-core generate-strategy /path/to/database.db > strategy.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example `strategy.json`:
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"users": {
|
|
59
|
+
"email": "unique_email",
|
|
60
|
+
"name": "full_name",
|
|
61
|
+
"phone": "phone_number"
|
|
62
|
+
},
|
|
63
|
+
"employees": {
|
|
64
|
+
"salary": "random_int"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Execute Anonymization
|
|
70
|
+
Anonymize a database file into a sanitized target output:
|
|
71
|
+
```bash
|
|
72
|
+
pynonymizer-core anonymize /path/to/database.db --strategy strategy.json --output /path/to/anonymized.db
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🤖 Model Context Protocol (MCP) Server
|
|
78
|
+
|
|
79
|
+
Pynonymizer Core includes an embedded Stdio MCP server compliant with JSON-RPC 2.0, giving AI agents direct tools to inspect schemas and sanitize datasets on demand.
|
|
80
|
+
|
|
81
|
+
### Provided Tools
|
|
82
|
+
|
|
83
|
+
| Tool Name | Description | Required Arguments |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `db_inspect_schema` | Returns tables, columns, types, and primary keys | `db_path` |
|
|
86
|
+
| `db_generate_strategy` | Evaluates schema and generates recommended GDPR masking map | `db_path` |
|
|
87
|
+
| `db_anonymize` | Applies declarative strategy to sanitize database | `source_db`, `strategy` (optional: `target_db`) |
|
|
88
|
+
|
|
89
|
+
### Claude Desktop / Cursor Setup
|
|
90
|
+
|
|
91
|
+
Add the following snippet to your `claude_desktop_config.json`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"pynonymizer-core": {
|
|
97
|
+
"command": "python",
|
|
98
|
+
"args": ["-m", "src.mcp.server"]
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Or using Docker:
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"mcpServers": {
|
|
108
|
+
"pynonymizer-core": {
|
|
109
|
+
"command": "docker",
|
|
110
|
+
"args": ["run", "-i", "--rm", "pynonymizer-core"]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🧪 Testing & Security Verification
|
|
119
|
+
|
|
120
|
+
Run the test suite:
|
|
121
|
+
```bash
|
|
122
|
+
python -m unittest discover -s tests
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Security Posture (Raptor Guard V2)
|
|
126
|
+
- **Zero Sinks / Leaks:** No outbound socket requests or analytics telemetry.
|
|
127
|
+
- **Prepared Statements:** Parameterized queries for deterministic execution.
|
|
128
|
+
- **Deterministic Tokenization:** Safe synthetic data replacement conforming to GDPR Art. 32 requirements.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 📄 License
|
|
133
|
+
|
|
134
|
+
MIT License. Developed and maintained by **THE HIVE // BSO** ([diamantenschmiede](https://github.com/tastenkasperle)).
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pynonymizer-core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Native Zero-Dependency In-Memory Database Anonymizer & GDPR Test Data Generator (CLI & MCP Server)
|
|
5
|
+
Author-email: THE HIVE // BSO <diamantenschmiede@proton.me>
|
|
6
|
+
Maintainer: tastenkasperle
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/tastenkasperle/pynonymizer_core
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/tastenkasperle/pynonymizer_core/issues
|
|
10
|
+
Project-URL: Repository, https://github.com/tastenkasperle/pynonymizer_core.git
|
|
11
|
+
Keywords: mcp,mcp-server,database,sqlite,anonymizer,gdpr,dsgvo,test-data,zero-dependency
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Database
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# 🛡️ Pynonymizer Core
|
|
24
|
+
|
|
25
|
+
[](https://opensource.org/licenses/MIT)
|
|
26
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
27
|
+
[](https://modelcontextprotocol.io)
|
|
28
|
+
[](https://github.com/tastenkasperle/pynonymizer_core)
|
|
29
|
+
[](https://gdpr-info.eu/art-32-gdpr/)
|
|
30
|
+
|
|
31
|
+
**Pynonymizer Core** is a native, zero-dependency in-memory SQLite database anonymization engine and GDPR/DSGVO test data generator designed for developers, CI/CD pipelines, and autonomous AI agents.
|
|
32
|
+
|
|
33
|
+
Freed from brittle host dependencies (`pg_dump`, `mysql` in `$PATH`) and heavy ORM overhead, Pynonymizer Core operates directly via Python's native `sqlite3` runtime—either in-memory or on disk with safe copy-on-write isolation.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🌟 Key Features
|
|
38
|
+
|
|
39
|
+
- **Zero External Dependencies:** Built purely on Python's standard library. Instant portability across Windows, Linux, and macOS without binary compilation.
|
|
40
|
+
- **Auto Strategy Inference:** Intelligently detects PII columns (names, emails, phone numbers, addresses, salaries, tax IDs) and generates declarative masking strategies.
|
|
41
|
+
- **In-Memory & File-Safe Operations:** Execute high-throughput anonymization entirely in RAM or create verified, scrubbed file copies.
|
|
42
|
+
- **Model Context Protocol (MCP) Ready:** First-class Stdio JSON-RPC 2.0 interface for Claude Desktop, Cursor, Antigravity, and autonomous agent frameworks.
|
|
43
|
+
- **Raptor Guard V2 Certified:** Hardened against SQL injection, column identifier tampering, and path traversal vectors.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🚀 Quickstart & Installation
|
|
48
|
+
|
|
49
|
+
Install directly via pip:
|
|
50
|
+
```bash
|
|
51
|
+
pip install .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or install from distribution:
|
|
55
|
+
```bash
|
|
56
|
+
pip install pynonymizer-core
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 💻 CLI Usage
|
|
62
|
+
|
|
63
|
+
The command-line interface provides lightweight, modular commands for inspection, rule generation, and execution.
|
|
64
|
+
|
|
65
|
+
### 1. Inspect Database Schema
|
|
66
|
+
Inspect tables, columns, data types, and primary keys:
|
|
67
|
+
```bash
|
|
68
|
+
pynonymizer-core inspect /path/to/database.db
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Auto-Generate Anonymization Strategy
|
|
72
|
+
Infer column semantics and export an editable strategy JSON file:
|
|
73
|
+
```bash
|
|
74
|
+
pynonymizer-core generate-strategy /path/to/database.db > strategy.json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Example `strategy.json`:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"users": {
|
|
81
|
+
"email": "unique_email",
|
|
82
|
+
"name": "full_name",
|
|
83
|
+
"phone": "phone_number"
|
|
84
|
+
},
|
|
85
|
+
"employees": {
|
|
86
|
+
"salary": "random_int"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Execute Anonymization
|
|
92
|
+
Anonymize a database file into a sanitized target output:
|
|
93
|
+
```bash
|
|
94
|
+
pynonymizer-core anonymize /path/to/database.db --strategy strategy.json --output /path/to/anonymized.db
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 🤖 Model Context Protocol (MCP) Server
|
|
100
|
+
|
|
101
|
+
Pynonymizer Core includes an embedded Stdio MCP server compliant with JSON-RPC 2.0, giving AI agents direct tools to inspect schemas and sanitize datasets on demand.
|
|
102
|
+
|
|
103
|
+
### Provided Tools
|
|
104
|
+
|
|
105
|
+
| Tool Name | Description | Required Arguments |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `db_inspect_schema` | Returns tables, columns, types, and primary keys | `db_path` |
|
|
108
|
+
| `db_generate_strategy` | Evaluates schema and generates recommended GDPR masking map | `db_path` |
|
|
109
|
+
| `db_anonymize` | Applies declarative strategy to sanitize database | `source_db`, `strategy` (optional: `target_db`) |
|
|
110
|
+
|
|
111
|
+
### Claude Desktop / Cursor Setup
|
|
112
|
+
|
|
113
|
+
Add the following snippet to your `claude_desktop_config.json`:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"pynonymizer-core": {
|
|
119
|
+
"command": "python",
|
|
120
|
+
"args": ["-m", "src.mcp.server"]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Or using Docker:
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"pynonymizer-core": {
|
|
131
|
+
"command": "docker",
|
|
132
|
+
"args": ["run", "-i", "--rm", "pynonymizer-core"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 🧪 Testing & Security Verification
|
|
141
|
+
|
|
142
|
+
Run the test suite:
|
|
143
|
+
```bash
|
|
144
|
+
python -m unittest discover -s tests
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Security Posture (Raptor Guard V2)
|
|
148
|
+
- **Zero Sinks / Leaks:** No outbound socket requests or analytics telemetry.
|
|
149
|
+
- **Prepared Statements:** Parameterized queries for deterministic execution.
|
|
150
|
+
- **Deterministic Tokenization:** Safe synthetic data replacement conforming to GDPR Art. 32 requirements.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 📄 License
|
|
155
|
+
|
|
156
|
+
MIT License. Developed and maintained by **THE HIVE // BSO** ([diamantenschmiede](https://github.com/tastenkasperle)).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pynonymizer_core.egg-info/PKG-INFO
|
|
5
|
+
pynonymizer_core.egg-info/SOURCES.txt
|
|
6
|
+
pynonymizer_core.egg-info/dependency_links.txt
|
|
7
|
+
pynonymizer_core.egg-info/entry_points.txt
|
|
8
|
+
pynonymizer_core.egg-info/top_level.txt
|
|
9
|
+
src/cli/main.py
|
|
10
|
+
src/core/anonymizer.py
|
|
11
|
+
src/core/generators.py
|
|
12
|
+
src/mcp/server.py
|
|
13
|
+
tests/test_pynonymizer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pynonymizer-core"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="THE HIVE // BSO", email="diamantenschmiede@proton.me" },
|
|
10
|
+
]
|
|
11
|
+
maintainers = [
|
|
12
|
+
{ name="tastenkasperle" },
|
|
13
|
+
]
|
|
14
|
+
description = "Native Zero-Dependency In-Memory Database Anonymizer & GDPR Test Data Generator (CLI & MCP Server)"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
license = { text = "MIT" }
|
|
17
|
+
requires-python = ">=3.9"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Database",
|
|
23
|
+
"Topic :: Security",
|
|
24
|
+
"Topic :: Software Development :: Testing",
|
|
25
|
+
]
|
|
26
|
+
keywords = ["mcp", "mcp-server", "database", "sqlite", "anonymizer", "gdpr", "dsgvo", "test-data", "zero-dependency"]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
"Homepage" = "https://github.com/tastenkasperle/pynonymizer_core"
|
|
31
|
+
"Bug Tracker" = "https://github.com/tastenkasperle/pynonymizer_core/issues"
|
|
32
|
+
"Repository" = "https://github.com/tastenkasperle/pynonymizer_core.git"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
pynonymizer-core = "src.cli.main:main"
|
|
36
|
+
pynonymizer-mcp = "src.mcp.server:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["src*"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pynonymizer CLI: Dünner Command-Line-Adapter.
|
|
3
|
+
Anti-Carbonara: Delegiert 100% der Arbeit an src.core.anonymizer.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import argparse
|
|
9
|
+
from src.core.anonymizer import DatabaseAnonymizer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(description="Pynonymizer Core - Native SQLite Anonymizer & Test Data Generator")
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
15
|
+
|
|
16
|
+
# Command: inspect
|
|
17
|
+
inspect_parser = subparsers.add_parser("inspect", help="Inspect database schema")
|
|
18
|
+
inspect_parser.add_argument("db", help="Path to SQLite database")
|
|
19
|
+
|
|
20
|
+
# Command: generate-strategy
|
|
21
|
+
gen_parser = subparsers.add_parser("generate-strategy", help="Auto-generate strategy JSON for database")
|
|
22
|
+
gen_parser.add_argument("db", help="Path to SQLite database")
|
|
23
|
+
|
|
24
|
+
# Command: anonymize
|
|
25
|
+
anon_parser = subparsers.add_parser("anonymize", help="Anonymize SQLite database")
|
|
26
|
+
anon_parser.add_argument("db", help="Source SQLite database")
|
|
27
|
+
anon_parser.add_argument("--strategy", required=True, help="Path to strategy JSON file")
|
|
28
|
+
anon_parser.add_argument("--output", help="Optional output database file (safe copy)", default=None)
|
|
29
|
+
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
if args.command == "inspect":
|
|
33
|
+
anon = DatabaseAnonymizer(args.db)
|
|
34
|
+
schema = anon.inspect_schema()
|
|
35
|
+
print(json.dumps(schema, indent=2))
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
elif args.command == "generate-strategy":
|
|
39
|
+
anon = DatabaseAnonymizer(args.db)
|
|
40
|
+
strategy = anon.generate_strategy()
|
|
41
|
+
print(json.dumps(strategy, indent=2))
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
elif args.command == "anonymize":
|
|
45
|
+
with open(args.strategy, "r", encoding="utf-8") as f:
|
|
46
|
+
strategy = json.load(f)
|
|
47
|
+
anon = DatabaseAnonymizer(args.db)
|
|
48
|
+
res = anon.anonymize(strategy, target_path=args.output)
|
|
49
|
+
print(json.dumps(res, indent=2))
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
return 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
sys.exit(main())
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pynonymizer Core: Native SQLite Anonymizer & Schema Inspector.
|
|
3
|
+
Executes in-place or file-to-file database anonymization using declarative rules.
|
|
4
|
+
Anti-Carbonara: Pure Logic, No CLI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
11
|
+
from src.core.generators import FakeDataGenerator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseAnonymizer:
|
|
15
|
+
def __init__(self, db_path: str):
|
|
16
|
+
self.db_path = self._validate_db_path(db_path)
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def _validate_db_path(path_str: str) -> str:
|
|
20
|
+
if not path_str or not isinstance(path_str, str):
|
|
21
|
+
raise ValueError("Invalid database path provided")
|
|
22
|
+
if "\x00" in path_str:
|
|
23
|
+
raise ValueError("Security Violation: Null byte in database path")
|
|
24
|
+
base = os.path.basename(os.path.abspath(path_str)).lower()
|
|
25
|
+
if base.startswith(".env") or base.startswith("id_rsa") or base.startswith("id_ed25519") or "masterpw" in base:
|
|
26
|
+
raise ValueError(f"Security Violation: Protected credential file blocked: {base}")
|
|
27
|
+
return path_str
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _quote_ident(ident: str) -> str:
|
|
31
|
+
if not isinstance(ident, str) or "\x00" in ident:
|
|
32
|
+
raise ValueError("Invalid SQL identifier")
|
|
33
|
+
return '"' + ident.replace('"', '""') + '"'
|
|
34
|
+
|
|
35
|
+
def inspect_schema(self) -> Dict[str, List[Dict[str, Any]]]:
|
|
36
|
+
"""Returns tables and their columns with types."""
|
|
37
|
+
conn = sqlite3.connect(self.db_path)
|
|
38
|
+
cursor = conn.cursor()
|
|
39
|
+
schema: Dict[str, List[Dict[str, Any]]] = {}
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';")
|
|
43
|
+
tables = [row[0] for row in cursor.fetchall()]
|
|
44
|
+
|
|
45
|
+
for t in tables:
|
|
46
|
+
cursor.execute(f"PRAGMA table_info({self._quote_ident(t)});")
|
|
47
|
+
cols = cursor.fetchall()
|
|
48
|
+
schema[t] = [
|
|
49
|
+
{"cid": c[0], "name": c[1], "type": c[2], "notnull": bool(c[3]), "pk": bool(c[5])}
|
|
50
|
+
for c in cols
|
|
51
|
+
]
|
|
52
|
+
finally:
|
|
53
|
+
conn.close()
|
|
54
|
+
|
|
55
|
+
return schema
|
|
56
|
+
|
|
57
|
+
def generate_strategy(self) -> Dict[str, Dict[str, str]]:
|
|
58
|
+
"""Automatically infers a strategy map based on column names."""
|
|
59
|
+
schema = self.inspect_schema()
|
|
60
|
+
strategy: Dict[str, Dict[str, str]] = {}
|
|
61
|
+
|
|
62
|
+
for table, cols in schema.items():
|
|
63
|
+
table_rules: Dict[str, str] = {}
|
|
64
|
+
for col in cols:
|
|
65
|
+
cname = col["name"].lower()
|
|
66
|
+
if "email" in cname or "mail" in cname:
|
|
67
|
+
table_rules[col["name"]] = "email"
|
|
68
|
+
elif "phone" in cname or "tel" in cname or "mobile" in cname:
|
|
69
|
+
table_rules[col["name"]] = "phone"
|
|
70
|
+
elif "first_name" in cname or "firstname" in cname or "vorname" in cname:
|
|
71
|
+
table_rules[col["name"]] = "first_name"
|
|
72
|
+
elif "last_name" in cname or "lastname" in cname or "nachname" in cname:
|
|
73
|
+
table_rules[col["name"]] = "last_name"
|
|
74
|
+
elif "name" in cname and not col["pk"]:
|
|
75
|
+
table_rules[col["name"]] = "full_name"
|
|
76
|
+
elif "company" in cname or "firma" in cname or "arbeitgeber" in cname:
|
|
77
|
+
table_rules[col["name"]] = "company"
|
|
78
|
+
elif "iban" in cname or "konto" in cname:
|
|
79
|
+
table_rules[col["name"]] = "iban"
|
|
80
|
+
|
|
81
|
+
if table_rules:
|
|
82
|
+
strategy[table] = table_rules
|
|
83
|
+
|
|
84
|
+
return strategy
|
|
85
|
+
|
|
86
|
+
def anonymize(self, strategy: Dict[str, Dict[str, str]], target_path: Optional[str] = None) -> Dict[str, Any]:
|
|
87
|
+
"""
|
|
88
|
+
Applies strategy to anonymize database.
|
|
89
|
+
If target_path is specified, creates a safe copy first and anonymizes the copy.
|
|
90
|
+
"""
|
|
91
|
+
work_path = self.db_path
|
|
92
|
+
if target_path:
|
|
93
|
+
target_path = self._validate_db_path(target_path)
|
|
94
|
+
shutil.copyfile(self.db_path, target_path)
|
|
95
|
+
work_path = target_path
|
|
96
|
+
|
|
97
|
+
conn = sqlite3.connect(work_path)
|
|
98
|
+
cursor = conn.cursor()
|
|
99
|
+
stats: Dict[str, int] = {}
|
|
100
|
+
total_rows_modified = 0
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
for table, rules in strategy.items():
|
|
104
|
+
if not isinstance(rules, dict) or not rules:
|
|
105
|
+
continue
|
|
106
|
+
# Verify table exists
|
|
107
|
+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,))
|
|
108
|
+
if not cursor.fetchone():
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
quoted_table = self._quote_ident(table)
|
|
112
|
+
|
|
113
|
+
# Find primary key or rowid for row identification
|
|
114
|
+
cursor.execute(f"PRAGMA table_info({quoted_table});")
|
|
115
|
+
cols_info = cursor.fetchall()
|
|
116
|
+
col_names = [c[1] for c in cols_info]
|
|
117
|
+
pk_cols = [c[1] for c in cols_info if c[5] > 0]
|
|
118
|
+
id_col = pk_cols[0] if pk_cols else "rowid"
|
|
119
|
+
quoted_id_col = self._quote_ident(id_col)
|
|
120
|
+
|
|
121
|
+
# Validate all strategy columns against actual schema columns to prevent SQL injection
|
|
122
|
+
for col_name in rules.keys():
|
|
123
|
+
if col_name not in col_names:
|
|
124
|
+
raise ValueError(f"Security Violation: Column '{col_name}' does not exist in table '{table}'")
|
|
125
|
+
|
|
126
|
+
# Safely construct queries with validated & quoted identifiers
|
|
127
|
+
cols_to_fetch = [id_col] + list(rules.keys())
|
|
128
|
+
select_sql = "SELECT " + ", ".join(self._quote_ident(c) for c in cols_to_fetch) + f" FROM {quoted_table}"
|
|
129
|
+
cursor.execute(select_sql)
|
|
130
|
+
rows = cursor.fetchall()
|
|
131
|
+
|
|
132
|
+
table_modified = 0
|
|
133
|
+
for row in rows:
|
|
134
|
+
row_id = row[0]
|
|
135
|
+
update_parts = []
|
|
136
|
+
params = []
|
|
137
|
+
|
|
138
|
+
for idx, (col_name, rule_type) in enumerate(rules.items(), start=1):
|
|
139
|
+
current_val = row[idx]
|
|
140
|
+
if current_val is None:
|
|
141
|
+
continue
|
|
142
|
+
generator = FakeDataGenerator.get_generator(rule_type)
|
|
143
|
+
try:
|
|
144
|
+
# Pass current value if mask/hash
|
|
145
|
+
fake_val = generator(current_val) if rule_type in ["mask", "hash"] else generator()
|
|
146
|
+
except Exception:
|
|
147
|
+
fake_val = generator()
|
|
148
|
+
|
|
149
|
+
update_parts.append(f"{self._quote_ident(col_name)} = ?")
|
|
150
|
+
params.append(fake_val)
|
|
151
|
+
|
|
152
|
+
if update_parts:
|
|
153
|
+
params.append(row_id)
|
|
154
|
+
update_sql = f"UPDATE {quoted_table} SET " + ", ".join(update_parts) + f" WHERE {quoted_id_col} = ?"
|
|
155
|
+
cursor.execute(update_sql, params)
|
|
156
|
+
table_modified += 1
|
|
157
|
+
|
|
158
|
+
stats[table] = table_modified
|
|
159
|
+
total_rows_modified += table_modified
|
|
160
|
+
|
|
161
|
+
conn.commit()
|
|
162
|
+
finally:
|
|
163
|
+
conn.close()
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"status": "success",
|
|
167
|
+
"database_file": work_path,
|
|
168
|
+
"tables_affected": len(stats),
|
|
169
|
+
"rows_anonymized": total_rows_modified,
|
|
170
|
+
"breakdown": stats
|
|
171
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pynonymizer Core: Generators & Strategy Engine.
|
|
3
|
+
Pure Python, Zero External Dependencies, 100% In-Memory.
|
|
4
|
+
Anti-Carbonara: Pure Logic, No CLI, No print(), No sys.exit().
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import random
|
|
9
|
+
import re
|
|
10
|
+
from typing import Dict, Any, List, Callable, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FakeDataGenerator:
|
|
14
|
+
"""Lightweight, deterministic and random fake data generators without bloated dependencies."""
|
|
15
|
+
|
|
16
|
+
FIRST_NAMES = ["Alex", "Chris", "Jordan", "Taylor", "Sam", "Morgan", "Robin", "Casey", "Kim", "Pat", "Felix", "Laura", "Max", "Sophie", "Lukas", "Elena"]
|
|
17
|
+
LAST_NAMES = ["Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann", "Koch", "Bauer", "Richter"]
|
|
18
|
+
COMPANIES = ["Nexus Tech", "Alpha Innovations", "Sol Solutions", "Vanguard Media", "Peak Systems", "Aurora Digital", "Titan Industries"]
|
|
19
|
+
DOMAINS = ["example.com", "testmail.org", "internal.local", "dummycorp.net"]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def first_name(cls) -> str:
|
|
23
|
+
return random.choice(cls.FIRST_NAMES)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def last_name(cls) -> str:
|
|
27
|
+
return random.choice(cls.LAST_NAMES)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def full_name(cls) -> str:
|
|
31
|
+
return f"{cls.first_name()} {cls.last_name()}"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def email(cls, prefix: Optional[str] = None) -> str:
|
|
35
|
+
name = prefix or f"{cls.first_name().lower()}.{cls.last_name().lower()}"
|
|
36
|
+
clean_name = re.sub(r'[^a-zA-Z0-9._]', '', name)
|
|
37
|
+
num = random.randint(100, 9999)
|
|
38
|
+
domain = random.choice(cls.DOMAINS)
|
|
39
|
+
return f"{clean_name}_{num}@{domain}"
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def phone(cls) -> str:
|
|
43
|
+
return f"+49 {random.randint(150, 179)} {random.randint(1000000, 9999999)}"
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def company(cls) -> str:
|
|
47
|
+
return random.choice(cls.COMPANIES)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def iban(cls) -> str:
|
|
51
|
+
account_num = f"{random.randint(1000000000, 9999999999)}"
|
|
52
|
+
return f"DE{random.randint(10, 99)}50010517{account_num}"
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def mask_string(cls, val: Any) -> str:
|
|
56
|
+
s = str(val)
|
|
57
|
+
if len(s) <= 2:
|
|
58
|
+
return "**"
|
|
59
|
+
return s[0] + "*" * (len(s) - 2) + s[-1]
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def hash_value(cls, val: Any) -> str:
|
|
63
|
+
return hashlib.sha256(str(val).encode("utf-8")).hexdigest()[:16]
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_generator(cls, rule_type: str) -> Callable[..., Any]:
|
|
67
|
+
mapping = {
|
|
68
|
+
"first_name": cls.first_name,
|
|
69
|
+
"last_name": cls.last_name,
|
|
70
|
+
"full_name": cls.full_name,
|
|
71
|
+
"email": cls.email,
|
|
72
|
+
"unique_email": cls.email,
|
|
73
|
+
"phone": cls.phone,
|
|
74
|
+
"company": cls.company,
|
|
75
|
+
"iban": cls.iban,
|
|
76
|
+
"mask": cls.mask_string,
|
|
77
|
+
"hash": cls.hash_value,
|
|
78
|
+
}
|
|
79
|
+
return mapping.get(rule_type.lower(), lambda val=None: "ANONYMIZED")
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pynonymizer MCP Server: Stdio-basierter JSON-RPC 2.0 Server.
|
|
3
|
+
Ermöglicht autonomen KI-Agenten die automatische Testdaten-Generierung und Anonymisierung.
|
|
4
|
+
Anti-Carbonara Standard: Pure Core Integration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from src.core.anonymizer import DatabaseAnonymizer
|
|
11
|
+
|
|
12
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PynonymizerMCPServer:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.tools = [
|
|
18
|
+
{
|
|
19
|
+
"name": "db_inspect_schema",
|
|
20
|
+
"description": "Inspects tables, columns, data types, and primary keys of a SQLite database.",
|
|
21
|
+
"inputSchema": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"properties": {
|
|
24
|
+
"db_path": {"type": "string", "description": "Absolute path to SQLite database file"}
|
|
25
|
+
},
|
|
26
|
+
"required": ["db_path"]
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "db_generate_strategy",
|
|
31
|
+
"description": "Analyzes column names in a database and generates an automatic GDPR/DSGVO masking strategy.",
|
|
32
|
+
"inputSchema": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"db_path": {"type": "string", "description": "Absolute path to SQLite database file"}
|
|
36
|
+
},
|
|
37
|
+
"required": ["db_path"]
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "db_anonymize",
|
|
42
|
+
"description": "Applies a masking strategy to anonymize sensitive columns in a SQLite database (creates safe copy if target_path given).",
|
|
43
|
+
"inputSchema": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"source_db": {"type": "string", "description": "Path to source SQLite database"},
|
|
47
|
+
"strategy": {"type": "object", "description": "Strategy map: {tableName: {columnName: ruleType}}"},
|
|
48
|
+
"target_db": {"type": "string", "description": "Optional path for safe anonymized copy"}
|
|
49
|
+
},
|
|
50
|
+
"required": ["source_db", "strategy"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
def handle_request(self, req: dict) -> dict:
|
|
56
|
+
req_id = req.get("id")
|
|
57
|
+
method = req.get("method")
|
|
58
|
+
params = req.get("params", {})
|
|
59
|
+
|
|
60
|
+
if method == "initialize":
|
|
61
|
+
return {
|
|
62
|
+
"jsonrpc": "2.0",
|
|
63
|
+
"id": req_id,
|
|
64
|
+
"result": {
|
|
65
|
+
"protocolVersion": "2024-11-05",
|
|
66
|
+
"capabilities": {"tools": {}},
|
|
67
|
+
"serverInfo": {"name": "pynonymizer-core", "version": "1.0.0"}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
elif method in ("notifications/initialized", "initialized") or (isinstance(method, str) and method.startswith("notifications/")) or req_id is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
elif method == "ping":
|
|
75
|
+
return {
|
|
76
|
+
"jsonrpc": "2.0",
|
|
77
|
+
"id": req_id,
|
|
78
|
+
"result": {}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
elif method == "tools/list":
|
|
82
|
+
return {
|
|
83
|
+
"jsonrpc": "2.0",
|
|
84
|
+
"id": req_id,
|
|
85
|
+
"result": {"tools": self.tools}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
elif method == "tools/call":
|
|
89
|
+
tool_name = params.get("name")
|
|
90
|
+
args = params.get("arguments", {})
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if tool_name == "db_inspect_schema":
|
|
94
|
+
anon = DatabaseAnonymizer(args.get("db_path"))
|
|
95
|
+
schema = anon.inspect_schema()
|
|
96
|
+
return {
|
|
97
|
+
"jsonrpc": "2.0",
|
|
98
|
+
"id": req_id,
|
|
99
|
+
"result": {"content": [{"type": "text", "text": json.dumps(schema, indent=2)}]}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
elif tool_name == "db_generate_strategy":
|
|
103
|
+
anon = DatabaseAnonymizer(args.get("db_path"))
|
|
104
|
+
strat = anon.generate_strategy()
|
|
105
|
+
return {
|
|
106
|
+
"jsonrpc": "2.0",
|
|
107
|
+
"id": req_id,
|
|
108
|
+
"result": {"content": [{"type": "text", "text": json.dumps(strat, indent=2)}]}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
elif tool_name == "db_anonymize":
|
|
112
|
+
anon = DatabaseAnonymizer(args.get("source_db"))
|
|
113
|
+
res = anon.anonymize(args.get("strategy"), target_path=args.get("target_db"))
|
|
114
|
+
return {
|
|
115
|
+
"jsonrpc": "2.0",
|
|
116
|
+
"id": req_id,
|
|
117
|
+
"result": {"content": [{"type": "text", "text": json.dumps(res, indent=2)}]}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
else:
|
|
121
|
+
return {
|
|
122
|
+
"jsonrpc": "2.0",
|
|
123
|
+
"id": req_id,
|
|
124
|
+
"error": {"code": -32601, "message": f"Method {tool_name} not found"}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logging.exception(f"Error executing tool {tool_name}")
|
|
129
|
+
return {
|
|
130
|
+
"jsonrpc": "2.0",
|
|
131
|
+
"id": req_id,
|
|
132
|
+
"error": {"code": -32000, "message": str(e)}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"jsonrpc": "2.0",
|
|
137
|
+
"id": req_id,
|
|
138
|
+
"error": {"code": -32600, "message": "Invalid Request"}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
def run(self):
|
|
142
|
+
logging.info("Pynonymizer MCP Server running on stdio.")
|
|
143
|
+
for line in sys.stdin:
|
|
144
|
+
line = line.strip()
|
|
145
|
+
if not line:
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
req = json.loads(line)
|
|
149
|
+
resp = self.handle_request(req)
|
|
150
|
+
if resp is not None:
|
|
151
|
+
sys.stdout.write(json.dumps(resp) + "\n")
|
|
152
|
+
sys.stdout.flush()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
server = PynonymizerMCPServer()
|
|
159
|
+
server.run()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit & Integration Tests for Pynonymizer Core.
|
|
3
|
+
Includes Simulation of Dienstplan-App (Employees, Shifts, Salaries) & Fire Team Pen-Tests.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import unittest
|
|
7
|
+
import sqlite3
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
import json
|
|
11
|
+
from src.core.anonymizer import DatabaseAnonymizer
|
|
12
|
+
from src.core.generators import FakeDataGenerator
|
|
13
|
+
from src.mcp.server import PynonymizerMCPServer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestPynonymizerCore(unittest.TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
# Create a temporary SQLite database simulating the Dienstplan-App
|
|
19
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
20
|
+
self.db_path = os.path.join(self.temp_dir, "dienstplan_test.db")
|
|
21
|
+
self.target_path = os.path.join(self.temp_dir, "dienstplan_anonymized.db")
|
|
22
|
+
|
|
23
|
+
conn = sqlite3.connect(self.db_path)
|
|
24
|
+
cur = conn.cursor()
|
|
25
|
+
# Employees table with sensitive PII
|
|
26
|
+
cur.execute("""
|
|
27
|
+
CREATE TABLE mitarbeiter (
|
|
28
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
+
vorname TEXT NOT NULL,
|
|
30
|
+
nachname TEXT NOT NULL,
|
|
31
|
+
email TEXT NOT NULL,
|
|
32
|
+
telefon TEXT,
|
|
33
|
+
iban TEXT,
|
|
34
|
+
abteilung TEXT
|
|
35
|
+
);
|
|
36
|
+
""")
|
|
37
|
+
# Insert sensitive mock employees
|
|
38
|
+
cur.execute("INSERT INTO mitarbeiter (vorname, nachname, email, telefon, iban, abteilung) VALUES (?, ?, ?, ?, ?, ?)",
|
|
39
|
+
("Max", "Mustermann", "max.mustermann@hospital.de", "+49 170 1234567", "DE89370400440532013000", "Station A"))
|
|
40
|
+
cur.execute("INSERT INTO mitarbeiter (vorname, nachname, email, telefon, iban, abteilung) VALUES (?, ?, ?, ?, ?, ?)",
|
|
41
|
+
("Erika", "Musterfrau", "erika.muster@hospital.de", "+49 171 7654321", "DE12500105170648489890", "Station B"))
|
|
42
|
+
conn.commit()
|
|
43
|
+
conn.close()
|
|
44
|
+
|
|
45
|
+
def tearDown(self):
|
|
46
|
+
import shutil
|
|
47
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
48
|
+
|
|
49
|
+
def test_schema_inspection(self):
|
|
50
|
+
anon = DatabaseAnonymizer(self.db_path)
|
|
51
|
+
schema = anon.inspect_schema()
|
|
52
|
+
self.assertIn("mitarbeiter", schema)
|
|
53
|
+
col_names = [c["name"] for c in schema["mitarbeiter"]]
|
|
54
|
+
self.assertIn("email", col_names)
|
|
55
|
+
self.assertIn("vorname", col_names)
|
|
56
|
+
self.assertIn("iban", col_names)
|
|
57
|
+
|
|
58
|
+
def test_auto_strategy_generation(self):
|
|
59
|
+
anon = DatabaseAnonymizer(self.db_path)
|
|
60
|
+
strategy = anon.generate_strategy()
|
|
61
|
+
self.assertIn("mitarbeiter", strategy)
|
|
62
|
+
rules = strategy["mitarbeiter"]
|
|
63
|
+
self.assertEqual(rules.get("email"), "email")
|
|
64
|
+
self.assertEqual(rules.get("vorname"), "first_name")
|
|
65
|
+
self.assertEqual(rules.get("nachname"), "last_name")
|
|
66
|
+
self.assertEqual(rules.get("telefon"), "phone")
|
|
67
|
+
self.assertEqual(rules.get("iban"), "iban")
|
|
68
|
+
|
|
69
|
+
def test_anonymize_safe_copy(self):
|
|
70
|
+
anon = DatabaseAnonymizer(self.db_path)
|
|
71
|
+
strategy = {
|
|
72
|
+
"mitarbeiter": {
|
|
73
|
+
"vorname": "first_name",
|
|
74
|
+
"nachname": "last_name",
|
|
75
|
+
"email": "unique_email",
|
|
76
|
+
"telefon": "phone",
|
|
77
|
+
"iban": "iban"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
res = anon.anonymize(strategy, target_path=self.target_path)
|
|
81
|
+
self.assertEqual(res["status"], "success")
|
|
82
|
+
self.assertEqual(res["rows_anonymized"], 2)
|
|
83
|
+
|
|
84
|
+
# Verify original is UNCHANGED
|
|
85
|
+
conn_orig = sqlite3.connect(self.db_path)
|
|
86
|
+
row_orig = conn_orig.cursor().execute("SELECT vorname, email FROM mitarbeiter WHERE id = 1").fetchone()
|
|
87
|
+
self.assertEqual(row_orig[0], "Max")
|
|
88
|
+
self.assertEqual(row_orig[1], "max.mustermann@hospital.de")
|
|
89
|
+
conn_orig.close()
|
|
90
|
+
|
|
91
|
+
# Verify target is ANONYMIZED
|
|
92
|
+
conn_target = sqlite3.connect(self.target_path)
|
|
93
|
+
row_target = conn_target.cursor().execute("SELECT vorname, email, iban, abteilung FROM mitarbeiter WHERE id = 1").fetchone()
|
|
94
|
+
self.assertNotEqual(row_target[0], "Max")
|
|
95
|
+
self.assertNotEqual(row_target[1], "max.mustermann@hospital.de")
|
|
96
|
+
self.assertEqual(row_target[3], "Station A") # Non-PII preserved!
|
|
97
|
+
conn_target.close()
|
|
98
|
+
|
|
99
|
+
def test_mcp_server_protocol(self):
|
|
100
|
+
server = PynonymizerMCPServer()
|
|
101
|
+
init_resp = server.handle_request({"jsonrpc": "2.0", "id": 0, "method": "initialize"})
|
|
102
|
+
self.assertEqual(init_resp["result"]["serverInfo"]["name"], "pynonymizer-core")
|
|
103
|
+
self.assertIsNone(server.handle_request({"jsonrpc": "2.0", "method": "notifications/initialized"}))
|
|
104
|
+
ping_resp = server.handle_request({"jsonrpc": "2.0", "id": "p1", "method": "ping"})
|
|
105
|
+
self.assertEqual(ping_resp["result"], {})
|
|
106
|
+
|
|
107
|
+
list_resp = server.handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/list"})
|
|
108
|
+
tools = [t["name"] for t in list_resp["result"]["tools"]]
|
|
109
|
+
self.assertIn("db_inspect_schema", tools)
|
|
110
|
+
self.assertIn("db_generate_strategy", tools)
|
|
111
|
+
self.assertIn("db_anonymize", tools)
|
|
112
|
+
|
|
113
|
+
# Test SQL injection defense on strategy column names
|
|
114
|
+
anon = DatabaseAnonymizer(self.db_path)
|
|
115
|
+
with self.assertRaises(ValueError):
|
|
116
|
+
anon.anonymize({"mitarbeiter": {'email" FROM sqlite_master --': "email"}})
|
|
117
|
+
|
|
118
|
+
# Test db_generate_strategy via MCP
|
|
119
|
+
call_resp = server.handle_request({
|
|
120
|
+
"jsonrpc": "2.0",
|
|
121
|
+
"id": 2,
|
|
122
|
+
"method": "tools/call",
|
|
123
|
+
"params": {
|
|
124
|
+
"name": "db_generate_strategy",
|
|
125
|
+
"arguments": {"db_path": self.db_path}
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
content = json.loads(call_resp["result"]["content"][0]["text"])
|
|
129
|
+
self.assertIn("mitarbeiter", content)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
unittest.main()
|