metacls 0.3.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.
Files changed (57) hide show
  1. metacls-0.3.0/LICENSE +21 -0
  2. metacls-0.3.0/PKG-INFO +383 -0
  3. metacls-0.3.0/README.md +332 -0
  4. metacls-0.3.0/pyproject.toml +127 -0
  5. metacls-0.3.0/setup.cfg +4 -0
  6. metacls-0.3.0/src/metacls/__init__.py +8 -0
  7. metacls-0.3.0/src/metacls/_theme.py +130 -0
  8. metacls-0.3.0/src/metacls/api/__init__.py +5 -0
  9. metacls-0.3.0/src/metacls/api/app.py +276 -0
  10. metacls-0.3.0/src/metacls/api/jobs.py +230 -0
  11. metacls-0.3.0/src/metacls/api/schemas.py +81 -0
  12. metacls-0.3.0/src/metacls/cleaner.py +260 -0
  13. metacls-0.3.0/src/metacls/cli.py +571 -0
  14. metacls-0.3.0/src/metacls/config.py +160 -0
  15. metacls-0.3.0/src/metacls/diff.py +61 -0
  16. metacls-0.3.0/src/metacls/engines/__init__.py +124 -0
  17. metacls-0.3.0/src/metacls/engines/base.py +55 -0
  18. metacls-0.3.0/src/metacls/engines/container.py +387 -0
  19. metacls-0.3.0/src/metacls/engines/ebml_riff.py +304 -0
  20. metacls-0.3.0/src/metacls/engines/exiftool.py +197 -0
  21. metacls-0.3.0/src/metacls/engines/image.py +86 -0
  22. metacls-0.3.0/src/metacls/engines/legacy_office.py +178 -0
  23. metacls-0.3.0/src/metacls/engines/media.py +154 -0
  24. metacls-0.3.0/src/metacls/engines/office.py +429 -0
  25. metacls-0.3.0/src/metacls/engines/ole2.py +195 -0
  26. metacls-0.3.0/src/metacls/engines/pdf.py +503 -0
  27. metacls-0.3.0/src/metacls/engines/svg.py +141 -0
  28. metacls-0.3.0/src/metacls/models.py +79 -0
  29. metacls-0.3.0/src/metacls/py.typed +0 -0
  30. metacls-0.3.0/src/metacls/report/__init__.py +165 -0
  31. metacls-0.3.0/src/metacls/report/templates/report.html.jinja +139 -0
  32. metacls-0.3.0/src/metacls/scanner.py +82 -0
  33. metacls-0.3.0/src/metacls/watch.py +307 -0
  34. metacls-0.3.0/src/metacls/web.py +386 -0
  35. metacls-0.3.0/src/metacls.egg-info/PKG-INFO +383 -0
  36. metacls-0.3.0/src/metacls.egg-info/SOURCES.txt +55 -0
  37. metacls-0.3.0/src/metacls.egg-info/dependency_links.txt +1 -0
  38. metacls-0.3.0/src/metacls.egg-info/entry_points.txt +2 -0
  39. metacls-0.3.0/src/metacls.egg-info/requires.txt +39 -0
  40. metacls-0.3.0/src/metacls.egg-info/top_level.txt +1 -0
  41. metacls-0.3.0/tests/test_api.py +175 -0
  42. metacls-0.3.0/tests/test_cleaner.py +140 -0
  43. metacls-0.3.0/tests/test_cli.py +192 -0
  44. metacls-0.3.0/tests/test_corpus.py +180 -0
  45. metacls-0.3.0/tests/test_deterministic.py +53 -0
  46. metacls-0.3.0/tests/test_diff.py +52 -0
  47. metacls-0.3.0/tests/test_engine_container.py +172 -0
  48. metacls-0.3.0/tests/test_engine_ebml_riff.py +178 -0
  49. metacls-0.3.0/tests/test_engine_image.py +77 -0
  50. metacls-0.3.0/tests/test_engine_legacy_office.py +74 -0
  51. metacls-0.3.0/tests/test_engine_media.py +56 -0
  52. metacls-0.3.0/tests/test_engine_office.py +202 -0
  53. metacls-0.3.0/tests/test_engine_pdf.py +198 -0
  54. metacls-0.3.0/tests/test_engine_svg.py +73 -0
  55. metacls-0.3.0/tests/test_scanner.py +62 -0
  56. metacls-0.3.0/tests/test_watch.py +156 -0
  57. metacls-0.3.0/tests/test_web.py +113 -0
metacls-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Görkem Güler
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.
metacls-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,383 @@
1
+ Metadata-Version: 2.4
2
+ Name: metacls
3
+ Version: 0.3.0
4
+ Summary: Bulk metadata scrubbing for PDF/Office/image files — the remediation companion to MetaScout.
5
+ Author: Görkem Güler
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/gorkemguler/MetaCLS
8
+ Project-URL: Repository, https://github.com/gorkemguler/MetaCLS
9
+ Project-URL: Changelog, https://github.com/gorkemguler/MetaCLS/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/gorkemguler/MetaCLS/issues
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Topic :: Security
15
+ Classifier: Topic :: Utilities
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: click>=8.1
20
+ Requires-Dist: pikepdf>=9.0
21
+ Requires-Dist: olefile>=0.47
22
+ Requires-Dist: rich>=13.7
23
+ Requires-Dist: jinja2>=3.1
24
+ Requires-Dist: python-dotenv>=1.0
25
+ Requires-Dist: flask>=3.0
26
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
27
+ Provides-Extra: api
28
+ Requires-Dist: fastapi>=0.110; extra == "api"
29
+ Requires-Dist: uvicorn>=0.29; extra == "api"
30
+ Requires-Dist: python-multipart>=0.0.9; extra == "api"
31
+ Provides-Extra: image-fallback
32
+ Requires-Dist: Pillow>=10.0; extra == "image-fallback"
33
+ Requires-Dist: pillow-heif>=0.16; extra == "image-fallback"
34
+ Provides-Extra: media
35
+ Requires-Dist: mutagen>=1.47; extra == "media"
36
+ Provides-Extra: archive
37
+ Requires-Dist: py7zr>=0.20; extra == "archive"
38
+ Provides-Extra: msg
39
+ Requires-Dist: extract-msg>=0.48; extra == "msg"
40
+ Provides-Extra: dev
41
+ Requires-Dist: pytest>=8.0; extra == "dev"
42
+ Requires-Dist: mutagen>=1.47; extra == "dev"
43
+ Requires-Dist: py7zr>=0.20; extra == "dev"
44
+ Requires-Dist: extract-msg>=0.48; extra == "dev"
45
+ Requires-Dist: httpx>=0.27; extra == "dev"
46
+ Requires-Dist: fastapi>=0.110; extra == "dev"
47
+ Requires-Dist: uvicorn>=0.29; extra == "dev"
48
+ Requires-Dist: python-multipart>=0.0.9; extra == "dev"
49
+ Requires-Dist: Pillow>=10.0; extra == "dev"
50
+ Dynamic: license-file
51
+
52
+ <p align="center">
53
+ <img src="assets/banner.svg" alt="MetaCLS" width="100%">
54
+ </p>
55
+
56
+ <p align="center">
57
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-2dd4a7.svg">
58
+ <img alt="Python" src="https://img.shields.io/badge/python-3.10%2B-2dd4a7.svg">
59
+ <img alt="Platforms" src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-7dd88f.svg">
60
+ <img alt="Status" src="https://img.shields.io/badge/status-active%20development-f6c454.svg">
61
+ </p>
62
+
63
+ <p align="center">
64
+ Bulk metadata scrubbing for PDF, Office and image files.<br>
65
+ Strip the metadata, keep the document — with a before/after proof report.
66
+ </p>
67
+
68
+ <p align="center">
69
+ <sub>Pairs with <a href="https://github.com/gorkemguler/MetaScout">MetaScout</a>: MetaScout <i>finds</i> the leaks, MetaCLS <i>fixes</i> them.</sub>
70
+ </p>
71
+
72
+ <p align="center"><sub>🇬🇧 English · <a href="README.tr.md">🇹🇷 Türkçe</a></sub></p>
73
+
74
+ <p align="center">
75
+ <img src="assets/screenshot-report.png" alt="MetaCLS before/after report — 3 files, 18 metadata fields removed, 0 residual" width="90%">
76
+ </p>
77
+
78
+ ---
79
+
80
+ ## What is this?
81
+
82
+ An organisation runs MetaScout against its own site and finds a pile of published PDFs and
83
+ Office documents leaking author names, internal file paths, software/OS fingerprints and GPS
84
+ coordinates. Now someone has to actually **clean those files**. That's MetaCLS.
85
+
86
+ Point it at a folder (or drag files into the web UI, or POST them to the API) and it:
87
+
88
+ 1. **scans** every supported file for embedded metadata,
89
+ 2. **strips** it — aggressively by default — writing cleaned copies (originals untouched) or
90
+ overwriting in place,
91
+ 3. **verifies** each cleaned file by re-scanning it, and
92
+ 4. **reports** exactly what was removed, per file, as JSON and a styled HTML page you can hand
93
+ to a security team as evidence.
94
+
95
+ It only touches **metadata** — the document's visible content (body text, images, a scanned
96
+ signature) is never modified.
97
+
98
+ ## What it removes
99
+
100
+ | Format | Engine | Removed |
101
+ | --- | --- | --- |
102
+ | **PDF** | [pikepdf](https://github.com/pikepdf/pikepdf) (QPDF) | `/Info` dictionary (Author, Title, Producer, Creator, CreationDate, …), the XMP metadata packet, `/PieceInfo` and other application-private data, page-level metadata, annotation authors + timestamps (`/T` `/M` `/CreationDate`), and the description + timestamps on embedded-file attachments. The file is **fully rewritten**, so values sitting in superseded cross-reference sections can't be recovered from the output. Encrypted PDFs need `--password`. With `--strip-form-values`: AcroForm field values and the XFA `<xfa:data>` packet too. |
103
+ | **Office** `.docx .xlsx .pptx` (+ macro-enabled `.docm .xlsm .pptm` and templates `.dotx .dotm .xltx .xltm .potx .potm`) | stdlib `zipfile` | `docProps/core.xml` (creator, lastModifiedBy, revision, timestamps), `docProps/app.xml` (Company, Manager, Template path), `docProps/custom.xml`, the embedded thumbnail, and Word revision-save-id fingerprints (`w:rsids`) from `settings.xml`. Dangling relationships and content-type overrides are pruned; per-member zip timestamps are normalised. `vbaProject.bin` is **kept** (breaking macros is worse than the small chance a name hides in it) and the report flags that the file was only partly scrubbed. |
104
+ | **OpenDocument** `.odt .ods .odp` | stdlib `zipfile` | `meta.xml` — initial-creator, creator, generator, editing-cycles/duration, timestamps, document statistics, user-defined fields — plus the `Thumbnails/` preview image (a rendered snapshot of the first page) and its `META-INF/manifest.xml` entry. |
105
+ | **Legacy Office** `.doc .xls .ppt` | `olefile` (pure Python) | The `\x05SummaryInformation` / `\x05DocumentSummaryInformation` property streams — author, last-saved-by, company, manager, template, title, timestamps, custom properties — are patched out **in place**: same file size, same format, same structure. `--in-place` works. If a container can't be parsed, MetaCLS falls back to a LibreOffice (`soffice`) re-render to `.docx/.xlsx/.pptx`. |
106
+ | **SVG** `.svg` | stdlib `xml` | `<metadata>` (RDF/Dublin-Core author/title/licence), `sodipodi:` / `inkscape:` / Adobe-Illustrator elements and attributes, and editor comments (`<!-- Created with … -->`). The drawing itself is untouched. |
107
+ | **Images** `.jpg .jpeg .png .gif .tif .tiff .heic .heif .webp` | [ExifTool](https://exiftool.org) | All EXIF / IPTC / XMP / GPS / MakerNotes, PNG/WebP text chunks and the JPEG/GIF comment block. The ICC colour profile and EXIF orientation are kept by default so the picture still renders correctly (`--no-keep-color-profile` / `--no-keep-orientation` to drop those too). HEIC also works via `pillow-heif` when exiftool is absent. |
108
+ | **Audio / video** `.mp3 .m4a .flac .ogg .opus .wav .aiff` / `.mp4 .mov .m4v .3gp .mkv .webm .avi` | `mutagen` / ExifTool / pure Python | Audio: all tags (ID3 / Vorbis / iTunes) and embedded cover art — `pip install 'metacls[media]'`. MP4-family video: exiftool clears the metadata atoms (`ItemList`, `Keys`, `UserData`, XMP — artist, `Make`/`Model` from a phone, GPS, `CreationDate`). **Matroska / WebM / AVI** (exiftool can't write these): the whole EBML `Tags` block and the `Info` title / dates / muxer-and-writer app names are blanked in place — overwritten with `Void` / `JUNK` padding of identical length, so the file length is unchanged and `--in-place` works; the track data is never touched. Not scanned by default — pass **`--media`** (or list the extensions in `--filetypes`). Spot-check playback. |
109
+ | **Containers** `.zip .eml .tar .tar.gz .tgz .tar.bz2 .tar.xz .7z .msg` | stdlib `zipfile` / `tarfile` / `email`; `py7zr` / `extract-msg` (optional) | With **`--recurse`**: each supported member of an archive, and each email attachment, is scrubbed with its own engine and the archive/message repacked. `.tar*` also has its per-member uid/gid/username/mtime headers normalised (a leak of the packer's identity). `.7z` needs `pip install 'metacls[archive]'`. `.msg` (Outlook) is **read-only** — `metacls inspect --recurse` lists what's inside (needs `metacls[msg]`); export to `.eml` to scrub. Non-scrubbable members pass through untouched. Nesting is followed (depth-limited). |
110
+
111
+ `--keep Title` (repeatable) spares a named field from the otherwise-aggressive strip.
112
+ `--backup` keeps `<name>.orig` next to an `--in-place` scrub.
113
+
114
+ Two **opt-in** flags go past metadata into identity data that's technically content:
115
+ `--strip-form-values` blanks PDF form values — AcroForm fields (`/V` `/DV`) and their cached
116
+ appearance, plus the `<xfa:data>` packet of an XFA form (the XFA template and schema are kept);
117
+ `--strip-office-authors` blanks Office tracked-change / comment **author names and dates**
118
+ (the change and comment text stays, so accept/reject still works).
119
+
120
+ ## Install
121
+
122
+ ```bash
123
+ pip install metacls # core: PDF + Office scrubbing
124
+ pip install 'metacls[api]' # + the REST API service
125
+ pip install 'metacls[media]' # + audio tag scrubbing (mutagen)
126
+ pip install 'metacls[archive]' # + .7z recursion (py7zr)
127
+ pip install 'metacls[msg]' # + read-only .msg inspection (extract-msg)
128
+ pip install 'metacls[image-fallback]' # + Pillow (weak image fallback if exiftool is absent)
129
+ ```
130
+
131
+ Image scrubbing needs the **exiftool** binary on `PATH`:
132
+
133
+ ```bash
134
+ brew install exiftool # macOS
135
+ sudo apt install libimage-exiftool-perl # Debian / Ubuntu
136
+ ```
137
+
138
+ PDF, Office (modern **and** legacy `.doc/.xls/.ppt`), ODF and SVG scrubbing are pure Python
139
+ and need nothing extra. **LibreOffice** (`soffice`) is only used as a fallback for a legacy
140
+ container the in-place patcher can't parse.
141
+
142
+ > Python 3.10+ is supported. On a brand-new Python where `pikepdf` has no wheel yet, install
143
+ > under 3.12 instead.
144
+
145
+ ## CLI
146
+
147
+ <p align="center">
148
+ <img src="assets/screenshot-cli.svg" alt="metacls inspect and metacls clean in a terminal" width="90%">
149
+ </p>
150
+
151
+ ### Inspect — see what's in the files (read-only)
152
+
153
+ ```bash
154
+ metacls inspect ./published-docs
155
+ metacls inspect leak.pdf report.docx --json
156
+ ```
157
+
158
+ Run this on the files MetaScout flagged to see exactly what they carry before you scrub.
159
+
160
+ ### Clean
161
+
162
+ ```bash
163
+ # default: originals untouched, cleaned copies written under ./metacls_cleaned/,
164
+ # mirroring the input tree, plus report.json + report.html
165
+ metacls clean ./published-docs
166
+
167
+ # overwrite the originals instead (asks first; -y to skip the prompt)
168
+ metacls clean ./published-docs --in-place
169
+
170
+ # preview only, change nothing
171
+ metacls clean ./published-docs --dry-run
172
+
173
+ # keep document titles, Turkish report
174
+ metacls clean ./published-docs --keep Title --report-lang tr
175
+
176
+ # just some files
177
+ metacls clean a.pdf b.docx c.jpg --out ./clean
178
+ ```
179
+
180
+ Useful flags: `--filetypes`, `--no-recursive`, `--out DIR`, `--keep FIELD`, `--dry-run`,
181
+ `--no-verify`, `--no-keep-color-profile`, `--no-keep-orientation`, `--report-lang en|tr`,
182
+ `--password` (encrypted PDFs), `--strip-pdf-id`, `--backup` (keep `<name>.orig` with `--in-place`),
183
+ `--strip-form-values`, `--strip-office-authors` (opt-in — see above),
184
+ `--jobs N` (scrub N files in parallel), `--quarantine DIR` (overwrite the original but move
185
+ it to `DIR/<date>/` first — recoverable, safer than `--in-place`),
186
+ `--policy publish|internal|minimal` (named presets),
187
+ `--exclude GLOB` (repeatable — skip files/dirs when walking),
188
+ `--no-follow-symlinks` (don't scrub a symlinked file), `--progress` (a progress bar), and the
189
+ group-level `metacls --debug …` (re-raise on the first failing file instead of recording it).
190
+
191
+ `metacls inspect` takes `--media` and `--recurse` too, so you can point it at an archive or a
192
+ video and see what's inside before scrubbing.
193
+
194
+ **Project config:** a `.metacls.toml` in the working directory or a parent (up to the git
195
+ root) sets defaults per command — CLI flags and env vars still win.
196
+
197
+ ```toml
198
+ [clean]
199
+ strip-office-authors = true
200
+ jobs = 4
201
+ keep = ["Title"]
202
+ ```
203
+
204
+ **Exit codes** (so it works as a CI gate): `0` clean · `1` a file errored · `2` a cleaned file
205
+ still carried metadata on the verify re-scan · `3` (`--check` only) metadata found.
206
+
207
+ ### Diff — track a directory over time
208
+
209
+ ```bash
210
+ metacls clean ./published --out ./scan-jan # once a month, into dated dirs
211
+ metacls clean ./published --out ./scan-feb
212
+ metacls diff ./scan-jan ./scan-feb # what changed?
213
+ ```
214
+
215
+ Shows files added/removed between the two runs and, the useful part, files where metadata
216
+ **reappeared** (someone re-saved the document in an editor). Exit code `1` if anything
217
+ regained metadata — drop it in a cron job.
218
+
219
+ ### Watch — keep a drop folder scrubbed
220
+
221
+ ```bash
222
+ metacls watch /srv/ftp/incoming --move-processed /srv/ftp/scrubbed --interval 10 --settle 5
223
+ metacls watch ./inbox --once # one pass, for cron
224
+ metacls watch ./inbox --pattern 'invoice-*.pdf' -j 4 # filter + parallel backlog
225
+ ```
226
+
227
+ A poll loop for an FTP/SFTP landing zone: a file is only touched once it has stopped
228
+ changing for `--settle` seconds (a half-finished upload is never scrubbed), state lives in
229
+ `<dir>/.metacls-watch.json` so a restart doesn't reprocess everything, and a file
230
+ re-dropped with a newer timestamp is handled again. A `.metacls-watch.lock` file keeps a
231
+ second watcher off the same directory (a lock left by a dead process is stolen).
232
+ `--pattern GLOB` (repeatable) narrows what's picked up; `-j/--jobs N` scrubs a backlog in
233
+ parallel; state entries for files that have since vanished are pruned each pass. Scrubs in
234
+ place by default; `--to DIR` writes cleaned copies instead. A systemd template unit is in
235
+ [`platform/linux/`](platform/linux/metacls-watch@.service).
236
+
237
+ ## Web UI
238
+
239
+ ```bash
240
+ metacls web # opens http://127.0.0.1:8770/
241
+ ```
242
+
243
+ Drag files onto the page, get them back scrubbed — individually or as a zip — with a
244
+ per-file before/after view and the full report. Past runs are listed under **History**.
245
+ Local, single-user, **no authentication** — don't expose it to a network.
246
+
247
+ <p align="center">
248
+ <img src="assets/screenshot-web.png" alt="MetaCLS local web UI — drag-and-drop file drop zone" width="90%">
249
+ </p>
250
+
251
+ ## REST API
252
+
253
+ For a Linux server, an FTP drop-box, or a CI pipeline that needs to hand files off to be
254
+ cleaned:
255
+
256
+ ```bash
257
+ pip install 'metacls[api]'
258
+ metacls api # http://127.0.0.1:8000/ · interactive docs at /docs
259
+ ```
260
+
261
+ ```bash
262
+ # what can this server scrub, and which optional tools are installed?
263
+ curl -sS http://127.0.0.1:8000/v1/formats
264
+
265
+ # submit (recurse: look inside .zip/.tar/.7z/.eml · media: also audio/video)
266
+ curl -sS -X POST http://127.0.0.1:8000/v1/clean \
267
+ -F files=@leak.pdf -F files=@report.docx -F report_lang=en -F recurse=true
268
+ # -> {"job_id": "...", "links": {...}}
269
+
270
+ # poll
271
+ curl -sS http://127.0.0.1:8000/v1/clean/<job_id>
272
+
273
+ # pull the cleaned files + report as a zip
274
+ curl -sSL -o cleaned.zip http://127.0.0.1:8000/v1/clean/<job_id>/download
275
+ ```
276
+
277
+ Jobs run in a bounded background thread pool; `--max-workers` / `--max-pending` size it.
278
+ Uploads stream to disk with `--max-upload-mb` / `--max-files` caps. `GET /v1/formats` and
279
+ `GET /v1/health` are open even when an API key is set.
280
+
281
+ **Auth:** `metacls api --api-key KEY` (or `METACLS_API_KEY`) requires that key on every
282
+ `/v1` route except `/v1/health` and `/v1/formats` — send it as `X-API-Key: KEY` or
283
+ `Authorization: Bearer KEY`. Both `api` and `web` **refuse to bind a non-loopback host**
284
+ (`0.0.0.0`, a LAN IP) with no auth unless you pass `--insecure`; the supported way to expose
285
+ either is to bind `127.0.0.1` and put a reverse proxy in front —
286
+ **[docs/reverse-proxy.md](docs/reverse-proxy.md)** has ready nginx / Caddy configs.
287
+
288
+ ```bash
289
+ metacls api --host 0.0.0.0 --api-key "$(openssl rand -hex 24)"
290
+ curl -H "X-API-Key: $KEY" -F files=@leak.pdf http://server:8000/v1/clean
291
+ ```
292
+
293
+ ## Docker
294
+
295
+ Pull the published image (built for amd64 + arm64 on every release):
296
+
297
+ ```bash
298
+ docker pull ghcr.io/gorkemguler/metacls:latest
299
+ ```
300
+
301
+ ```bash
302
+ # web UI
303
+ docker run --rm -p 127.0.0.1:8770:8770 -v "$(pwd)/metacls_cleaned:/data" \
304
+ ghcr.io/gorkemguler/metacls:latest
305
+
306
+ # REST API
307
+ docker run --rm -p 127.0.0.1:8000:8000 -v "$(pwd)/metacls_cleaned:/data" \
308
+ ghcr.io/gorkemguler/metacls:latest api --host 0.0.0.0 --port 8000 --output-dir /data
309
+
310
+ # one-off: scrub a mounted folder
311
+ docker run --rm -v "$(pwd)/docs:/work" ghcr.io/gorkemguler/metacls:latest \
312
+ clean /work --out /work/cleaned
313
+ ```
314
+
315
+ Or build locally: `docker build -t metacls .`. `docker compose up --build` runs the web UI;
316
+ `docker compose --profile api up metacls-api` the API. Put a proxy in front before exposing
317
+ either — see [docs/reverse-proxy.md](docs/reverse-proxy.md).
318
+
319
+ ## How thorough is it?
320
+
321
+ - **PDF** — a full QPDF rewrite, not an incremental update, so the removed `/Info` and XMP
322
+ aren't left behind in an old xref section. Annotation authors/dates and embedded-file
323
+ metadata go too; the annotation's visible text and the attached file itself stay.
324
+ **Signed PDFs are skipped, not broken** — scrubbing would invalidate the signature;
325
+ re-export an unsigned copy if you need it cleaned. **Encrypted PDFs** without `--password`
326
+ are skipped; with it, the cleaned copy is written unencrypted (the result says so).
327
+ - **Office / ODF** — the metadata parts are deleted from the package (or, for ODF, emptied),
328
+ not merely blanked, and the references to them are pruned so nothing dangles.
329
+ - **Images** — `exiftool -all=`, which is the reference tool for this.
330
+ - **`--verify`** (on by default) re-scans every cleaned file and lists anything still present
331
+ in the report; the CLI exits `2` if so.
332
+ - **Deterministic** — scrubbing the same file twice with the same options gives byte-identical
333
+ output (checked in CI), so a scrub is auditable. `--strip-pdf-id` is the deliberate exception.
334
+
335
+ ### Limitations
336
+
337
+ - Content is out of scope by design: text in the document body, a visible/scanned signature,
338
+ text baked into an image — MetaCLS won't touch those. (MetaScout's `--scan-content` finds
339
+ them; removing them is a manual edit.)
340
+ - Legacy `.doc / .xls / .ppt` are not scrubbed (convert first).
341
+ - Not a certified sanitisation tool. Verify anything high-stakes yourself — that's what
342
+ `metacls inspect` on the output, or a second pass with MetaScout, is for.
343
+
344
+ ## Desktop integration
345
+
346
+ All in **[`platform/`](platform/)**, one install command each — everything scrubs in place:
347
+
348
+ - **macOS** — a drag-and-drop `MetaCLS.app` (built with `osacompile`, no Xcode), plus a
349
+ Finder **Quick Action**.
350
+ - **Windows** — a WinForms **drop window**, a **Send to** menu entry, an Explorer
351
+ **right-click** entry, and a `winget` manifest (template).
352
+ - **Linux** — a `.desktop` launcher / *Open With* handler (with a `zenity` picker), a
353
+ Nautilus/Nemo/Caja **script**, and a `systemd` **watch** unit.
354
+
355
+ ## CI / hooks
356
+
357
+ `metacls clean --check` implies `--dry-run` and exits **3** if any file still carries
358
+ metadata (0 if clean, 1 on error) — a gate for pre-commit and CI.
359
+
360
+ ```yaml
361
+ # .pre-commit-config.yaml
362
+ - repo: https://github.com/gorkemguler/MetaCLS
363
+ rev: main
364
+ hooks: [{ id: metacls }]
365
+ ```
366
+
367
+ ```yaml
368
+ # .github/workflows/no-metadata.yml
369
+ - uses: gorkemguler/MetaCLS@main
370
+ with:
371
+ paths: docs/ public/
372
+ mode: check # or "fix" to scrub in place and commit
373
+ ```
374
+
375
+ ## Roadmap
376
+
377
+ See **[ROADMAP.md](ROADMAP.md)** for what's shipped, the known limitations, and the backlog
378
+ (inotify watch, SFTP mode, `--jobs`, `--quarantine`, `.metacls.toml`, policy profiles,
379
+ audio/video, PyPI, recursive-container scrubbing).
380
+
381
+ ## License
382
+
383
+ MIT — see [LICENSE](LICENSE).