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.
- metacls-0.3.0/LICENSE +21 -0
- metacls-0.3.0/PKG-INFO +383 -0
- metacls-0.3.0/README.md +332 -0
- metacls-0.3.0/pyproject.toml +127 -0
- metacls-0.3.0/setup.cfg +4 -0
- metacls-0.3.0/src/metacls/__init__.py +8 -0
- metacls-0.3.0/src/metacls/_theme.py +130 -0
- metacls-0.3.0/src/metacls/api/__init__.py +5 -0
- metacls-0.3.0/src/metacls/api/app.py +276 -0
- metacls-0.3.0/src/metacls/api/jobs.py +230 -0
- metacls-0.3.0/src/metacls/api/schemas.py +81 -0
- metacls-0.3.0/src/metacls/cleaner.py +260 -0
- metacls-0.3.0/src/metacls/cli.py +571 -0
- metacls-0.3.0/src/metacls/config.py +160 -0
- metacls-0.3.0/src/metacls/diff.py +61 -0
- metacls-0.3.0/src/metacls/engines/__init__.py +124 -0
- metacls-0.3.0/src/metacls/engines/base.py +55 -0
- metacls-0.3.0/src/metacls/engines/container.py +387 -0
- metacls-0.3.0/src/metacls/engines/ebml_riff.py +304 -0
- metacls-0.3.0/src/metacls/engines/exiftool.py +197 -0
- metacls-0.3.0/src/metacls/engines/image.py +86 -0
- metacls-0.3.0/src/metacls/engines/legacy_office.py +178 -0
- metacls-0.3.0/src/metacls/engines/media.py +154 -0
- metacls-0.3.0/src/metacls/engines/office.py +429 -0
- metacls-0.3.0/src/metacls/engines/ole2.py +195 -0
- metacls-0.3.0/src/metacls/engines/pdf.py +503 -0
- metacls-0.3.0/src/metacls/engines/svg.py +141 -0
- metacls-0.3.0/src/metacls/models.py +79 -0
- metacls-0.3.0/src/metacls/py.typed +0 -0
- metacls-0.3.0/src/metacls/report/__init__.py +165 -0
- metacls-0.3.0/src/metacls/report/templates/report.html.jinja +139 -0
- metacls-0.3.0/src/metacls/scanner.py +82 -0
- metacls-0.3.0/src/metacls/watch.py +307 -0
- metacls-0.3.0/src/metacls/web.py +386 -0
- metacls-0.3.0/src/metacls.egg-info/PKG-INFO +383 -0
- metacls-0.3.0/src/metacls.egg-info/SOURCES.txt +55 -0
- metacls-0.3.0/src/metacls.egg-info/dependency_links.txt +1 -0
- metacls-0.3.0/src/metacls.egg-info/entry_points.txt +2 -0
- metacls-0.3.0/src/metacls.egg-info/requires.txt +39 -0
- metacls-0.3.0/src/metacls.egg-info/top_level.txt +1 -0
- metacls-0.3.0/tests/test_api.py +175 -0
- metacls-0.3.0/tests/test_cleaner.py +140 -0
- metacls-0.3.0/tests/test_cli.py +192 -0
- metacls-0.3.0/tests/test_corpus.py +180 -0
- metacls-0.3.0/tests/test_deterministic.py +53 -0
- metacls-0.3.0/tests/test_diff.py +52 -0
- metacls-0.3.0/tests/test_engine_container.py +172 -0
- metacls-0.3.0/tests/test_engine_ebml_riff.py +178 -0
- metacls-0.3.0/tests/test_engine_image.py +77 -0
- metacls-0.3.0/tests/test_engine_legacy_office.py +74 -0
- metacls-0.3.0/tests/test_engine_media.py +56 -0
- metacls-0.3.0/tests/test_engine_office.py +202 -0
- metacls-0.3.0/tests/test_engine_pdf.py +198 -0
- metacls-0.3.0/tests/test_engine_svg.py +73 -0
- metacls-0.3.0/tests/test_scanner.py +62 -0
- metacls-0.3.0/tests/test_watch.py +156 -0
- 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).
|