Commit Graph
58 Commits
Author SHA1 Message Date
Guillaume Meyer (The Opinionated Man) c19be20eac docs: add MarkLLM to README references 2026-08-14 08:46:09 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub ba1a36db0b Merge branch 'main' into fix/layer-a-script-glue-pua 2026-08-14 08:07:53 -07:00
Guillaume Meyer (The Opinionated Man) 90c1eac926 fix: preserve load-bearing script invisibles, strip PUA in Layer A (#38)
Mongolian FVS (U+180B-180D), Khmer inherent vowels (U+17B4/17B5) and
Hangul fillers (U+115F/1160) are orthography, not hidden marks, when
they follow a base from their own script. STRIP_CODEPOINTS removed them
unconditionally, corrupting real text (FVS glyph selection, Khmer
phonemic vowels, Hangul partial-syllable composition).

Mirror the existing #28 glue machinery: keep them after a same-script
base, still strip them when floating between unrelated characters, and
honor --strip-emoji-glue for paranoid mode. Add them to _is_glue so
chained FVS stay bound to their base.

Private-use codepoints (U+E000-F8FF and the supplementary PUA planes)
render as nothing portable and carry no agreed meaning, making them a
serviceable hiding place; they are Co, so they fell outside the Cf
strip path. Strip them by default with a new 'private_use' inspect kind.

Refs #38.
2026-08-14 08:02:59 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub a0dbbc8874 fix: SECURITY.md only references the private advisories channel (#51) 2026-08-14 08:02:13 -07:00
Guillaume Meyer (The Opinionated Man) 9321ace91f fix: SECURITY.md only references the private advisories channel 2026-08-14 07:59:43 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub cc6dd2b26d feat: print filename in inspect_file output (#50)
Add the resolved path at the top of every human report (File: line) and
include path in JSON output for all kinds, so batch inspection via find
can attribute hits to a file. Closes #31.
2026-08-14 07:40:34 -07:00
28eca2d91f fix: markdown frontmatter cleaner crashes and leaks nested AI keys (#25)
clean_markdown() had two bugs in the same loop:

1. It crashed with IndexError on any frontmatter containing a blank
   line, because line[0] was evaluated on an empty string.
2. It kept the nested children of a dropped top-level key, so dropping
   `model:` left `name: claude-opus` behind and produced invalid YAML,
   while still reporting the key as removed.

Rewrite the loop with a `dropping` flag that tracks whether the current
top-level key was dropped, and guard blank/comment lines before
indexing. inspect_markdown() is unchanged (already guards line[0]).

Adds 4 regression tests: blank-line crash, nested-key leak, inspect
round-trip, and comment/list preservation.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
2026-08-13 19:06:28 -07:00
58a958decd feat: preserve script joiners, flag emoji, and Arabic Cf marks in Layer A (#28)
Layer A already preserves emoji ZWJ/VS glue, but still stripped other
load-bearing invisibles, silently corrupting real text:

- ZWNJ/ZWJ inside complex scripts (Persian می‌روم, Devanagari क्‍ष)
- flag emoji tag sequences (🏴󠁧󠁢󠁳󠁣󠁴󠁿 -> 🏴)
- orthographic Arabic/Syriac Cf marks (U+0600, U+06DD, U+070F, ...)

Extend the existing _decide()/glue machinery: keep ZWNJ/ZWJ when a
neighbour is a complex-script letter, keep tag chars after an emoji
base, and allowlist the orthographic Cf codepoints. The same characters
between plain ASCII stay carriers and are still stripped.
--strip-emoji-glue continues to strip all of them (paranoid mode).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
2026-08-13 18:46:03 -07:00
ba42162b66 fix: --json no longer suppresses the residual-signal exit code (#30)
clean_file.py and clean_image.py computed the failure exit code inside
the human-output branch, so `--json` always exited 0 even when the clean
left C2PA/AI signals behind. A script gating on `clean_file --json` would
treat a still-marked file as clean.

Move the residual (and degraded-PDF) decision out of the output branch in
both entry points so the exit code is the same regardless of --json.
Human output is unchanged; degraded best-effort PDF copies stay
non-failures.

Adds tests asserting json and human modes return the same exit code for
residual, clean, and degraded cases.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:44:42 -07:00
256d90d1b1 Refuse binary input in the text-only tools (#24)
* Refuse binary input in the text-only tools

inspect_text.py, clean_text.py and rewrite_text.py accept any path and decode
it with errors="surrogateescape". Pointed at a .docx - a zip - they walk
deflate-compressed bytes and report whatever codepoints fall out of them. The
counts look like findings but track the compression, not the content: in one
sample set a document with nothing hidden in its text reported 12 "suspicious"
characters, while another with 54 real no-break spaces reported 11, none of
which were the no-break spaces.

clean_text.py is worse than misleading. It writes the mangled decode back, so
`clean_text.py report.docx` reports "removed=1" and silently corrupts the
document - the output still passes zipfile.is_zipfile() because the end-of-
central-directory record survives, but reading a member raises.

common.looks_binary() now sniffs magic numbers plus a control-byte ratio, and
guard_binary() refuses with a message naming the tool that does handle the
format. The ratio test is deliberately conservative so text in encodings other
than UTF-8 keeps working, and every entry point takes --force-text to override.
clean_file.py gets the same check on the branch where classify() falls back to
"text" for unrecognised bytes.

Adds tests covering magic-number and heuristic detection, the override, refusal
without writing or backing up, and that clean_file.py still routes a .docx to
the container path.

* Address review: backup ordering, stdin sniff, router advice

Three fixes from the review on #24.

clean_file.py sniffed after --in-place had already taken the backup, so
`clean_file.py --in-place mystery.bin` left a mystery.bin.bak sidecar behind
before exiting 2 — for a file the run never touches, and exactly what
clean_text.py avoids. The sniff now runs before backup_path(). The same hole
applied to `--as text` on a .docx, which bypasses classify() entirely.

The stdin path decoded before sniffing, which made detection depend on the
console codec. It was worse than codec drift: the text layer also translates
newlines, so PNG's `\x89PNG\r\n\x1a\n` arrived as `\x89PNG\n\x1a\n` and the
magic number never matched — the file was refused by the NUL-byte heuristic
instead, and would have sailed through had it lacked NULs. _read_stdin_capped
now reads sys.stdin.buffer and guards the raw octets, matching the file path,
with a text fallback for a replaced stdin.

guard_binary always advised "Use inspect_file.py / clean_file.py", which is
circular when the caller is one of them and classify() has already ruled out
every known container. The advice is now a parameter: the text-only scripts
keep the pointer to the routers, and the routers say the bytes match no
supported format and point at --force-text / --as.

Adds tests for the backup ordering (both --in-place paths), the advice split,
and stdin magic that is not ASCII, across default, cp1252 and latin-1 stdio
codecs — the previous stdin test piped a ZIP, whose "PK" header is ASCII and
survives any of them.

---------

Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
2026-08-13 13:35:00 -07:00
Guillaume Meyer (The Opinionated Man) e7e3b4ec90 docs: add stars/forks shields and drop star-history chart 2026-08-13 09:00:19 -07:00
Guillaume Meyer (The Opinionated Man) c267d785e8 docs: finalize v0.4.0 changelog and latest-release pointer v0.4.0 2026-08-13 08:56:43 -07:00
Guillaume Meyer (The Opinionated Man) 40b160bb2a test: fix Windows venv path in CtrlRegen runner test
The runner prefers .venv/Scripts/python.exe on Windows, but the test
hardcoded the POSIX .venv/bin/python layout, so it fell back to
sys.executable and failed the Windows CI leg.
2026-08-13 08:56:43 -07:00
Guillaume Meyer (The Opinionated Man) 8d8fe7ad84 Add optional CtrlRegen pixel removal (external noai-watermark backend)
Wires a standalone clean_ctrlregen.py adapter plus setup_ctrlregen.sh bootstrap, Dockerfile, Makefile targets, and clean_image.py --remove-pixel ctrlregen. The backend is cloned at a pinned commit and never bundled (noai-watermark ships no LICENSE file). Includes mock-based tests and docs with research references.
2026-08-13 08:53:53 -07:00
Guillaume Meyer (The Opinionated Man) 396c83dbae feat: add finding confidence and aggregate audits
- classify findings as confirmed/probable/informational/likely_false_positive
- expose confidence in text/image/container JSON and human reports
- add audit_dir.py and audit_website.py for aggregate reports
- document the confidence taxonomy and audit commands in SKILL.md

Closes #13
2026-08-13 08:29:04 -07:00
Guillaume Meyer (The Opinionated Man) 44606f6b64 docs: add watermark-removal references and responsible-use disclaimer 2026-08-13 08:29:04 -07:00
Guillaume Meyer (The Opinionated Man) df487bbc14 docs: add controllable-regeneration watermark removal reference 2026-08-13 08:29:04 -07:00
Guillaume Meyer (The Opinionated Man) ca375cec29 fix: stop AI-marker false positives in DOCX/emoji/HTML/PDF scans
- DOCX: scan only docProps/customXml parts, not the visible body (#14)
- Text Layer A: preserve emoji VS16/ZWJ after an emoji base; add paranoid flag (#22)
- HTML: treat CMS generator tags as informational, not AI metadata (#13)
- PDF: exclude stream payloads from AI-marker byte scan (#13)
- Add unsupported/best-effort notes to inspect reports
2026-08-13 08:29:04 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub b4676e9e54 fix: finish Windows support (preexec_fn, symlink tests, Windows CI) (#23)
* fix: make subprocess and symlink tests Windows-compatible

- Gate preexec_fn (POSIX-only) so optional tools can run on Windows
- Skip symlink tests when the platform denies the privilege
- Add a Windows CI leg and a Windows CLI smoke run

* fix: use UTF-8 stdio and fail Windows smoke on first error

- Reconfigure stdin/stdout/stderr to UTF-8 so Windows redirected streams
  stop raising UnicodeEncodeError on invisible Unicode characters
- Chain the Windows smoke commands so a failing script fails the step
2026-08-13 07:16:13 -07:00
6e5f9ce84e fix: guard os.fchmod for non-POSIX platforms (#15)
safe_write_bytes() calls os.fchmod() to restore the umask-default mode
after mkstemp() creates the temp file as 0600. os.fchmod does not exist
on Windows, so every write path raised:

    AttributeError: module 'os' has no attribute 'fchmod'

That broke clean_text.py, clean_file.py, clean_image.py and
rewrite_text.py whenever an output file was requested.

Windows has no POSIX mode bits to restore, so the call is skipped there.
The atomic-write guarantee is unaffected: the symlink refusal and
os.replace() are what protect the destination, not the chmod.

Adds a regression test that deletes os.fchmod via monkeypatch, so the
case is covered on POSIX CI too.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 06:37:57 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub f10efaa7ef Merge pull request #11 from dependabot
chore(deps): bump pywavelets from 1.7.0 to 1.9.0 in /skills/remove-ai-marks/scripts
2026-08-12 21:32:04 -07:00
dependabot[bot]andGitHub 9dc6d13b0b chore(deps): bump pywavelets in /skills/remove-ai-marks/scripts
Bumps [pywavelets](https://github.com/PyWavelets/pywt) from 1.7.0 to 1.9.0.
- [Release notes](https://github.com/PyWavelets/pywt/releases)
- [Commits](https://github.com/PyWavelets/pywt/compare/v1.7.0...v1.9.0)

---
updated-dependencies:
- dependency-name: pywavelets
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:31:55 +00:00
Guillaume Meyer (The Opinionated Man)andGitHub 9bfb639bdb Merge pull request #10 from dependabot
chore(deps): bump numpy from 2.0.2 to 2.5.2 in /skills/remove-ai-marks/scripts
2026-08-12 21:30:41 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub 32edee5694 Merge pull request #7 from dependabot
chore(deps): bump python from 3.11-slim to 3.14-slim
2026-08-12 21:30:09 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub de559611de Merge pull request #12 from dependabot
chore(deps): bump scikit-learn from 1.5.2 to 1.9.0 in /skills/remove-ai-marks/scripts
2026-08-12 21:30:04 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub 4bbfa3a4e0 Merge pull request #9 from dependabot
chore(deps): bump opencv-python from 4.10.0.84 to 5.0.0.93 in /skills/remove-ai-marks/scripts
2026-08-12 21:29:57 -07:00
dependabot[bot]andGitHub 4829a796c8 chore(deps): bump numpy in /skills/remove-ai-marks/scripts
Bumps [numpy](https://github.com/numpy/numpy) from 2.0.2 to 2.5.2.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst)
- [Commits](https://github.com/numpy/numpy/compare/v2.0.2...v2.5.2)

---
updated-dependencies:
- dependency-name: numpy
  dependency-version: 2.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:27:52 +00:00
dependabot[bot]andGitHub 4eb38cf27c chore(deps): bump scikit-learn in /skills/remove-ai-marks/scripts
Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.5.2 to 1.9.0.
- [Release notes](https://github.com/scikit-learn/scikit-learn/releases)
- [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.5.2...1.9.0)

---
updated-dependencies:
- dependency-name: scikit-learn
  dependency-version: 1.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:27:51 +00:00
dependabot[bot]andGitHub 96e97d2e8a chore(deps): bump opencv-python in /skills/remove-ai-marks/scripts
Bumps [opencv-python](https://github.com/opencv/opencv-python) from 4.10.0.84 to 5.0.0.93.
- [Release notes](https://github.com/opencv/opencv-python/releases)
- [Commits](https://github.com/opencv/opencv-python/commits)

---
updated-dependencies:
- dependency-name: opencv-python
  dependency-version: 5.0.0.93
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:27:50 +00:00
Guillaume Meyer (The Opinionated Man)andGitHub 67d3362339 Merge pull request #8 from dependabot
chore(deps): bump scipy from 1.14.1 to 1.18.0 in /skills/remove-ai-marks/scripts
2026-08-12 21:25:44 -07:00
dependabot[bot]andGitHub 42876469bc chore(deps): bump scipy in /skills/remove-ai-marks/scripts
Bumps [scipy](https://github.com/scipy/scipy) from 1.14.1 to 1.18.0.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](https://github.com/scipy/scipy/compare/v1.14.1...v1.18.0)

---
updated-dependencies:
- dependency-name: scipy
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:17:37 +00:00
dependabot[bot]andGitHub 2de8aa9abf chore(deps): bump python from 3.11-slim to 3.14-slim
Bumps python from 3.11-slim to 3.14-slim.

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-13 04:17:21 +00:00
Guillaume Meyer (The Opinionated Man) ccdde45f6d ci: add Dependabot config and security-path CODEOWNERS
- dependabot.yml: weekly updates for GitHub Actions (rotates the SHA pins),
  root pip deps (requirements-dev.txt), and the scorer requirements
  (compatibility-coupled to the pinned upstream commit — human re-verify
  noted), monthly for the digest-pinned Docker base image.
- CODEOWNERS: keep the default maintainer ownership and make ownership of
  /.github and SECURITY.md explicit.
2026-08-12 21:16:36 -07:00
Guillaume Meyer (The Opinionated Man) 34525246e2 Release v0.3.2: security hardening — safe writes, hardened HTTP client, CI supply chain v0.3.2 2026-08-12 21:14:47 -07:00
Guillaume Meyer (The Opinionated Man) fa81cbc23a fix: harden writes, HTTP client, CI supply chain, and resource caps
Security hardening from deep assessment:

- Writes: atomic temp+rename via safe_write_bytes/text (no symlink
  following, no partial in-place loss); backup_path for --in-place;
  umask-default modes; symlink destinations refused.
- rewrite_text: refuse redirects (no Authorization/key re-send to
  unvalidated hosts), default-deny loopback allowlist with explicit
  --allow-remote / WATERMARKS_REWRITE_ALLOW_REMOTE opt-in, http(s)-only
  schemes, --api-key argv flag removed (env-only).
- CI: SHA-pin actions (checkout v7.0.1, setup-python v7.0.0, codeql),
  permissions: contents: read, pinned requirements-dev.txt, pip-audit
  step; new CodeQL workflow.
- Scorer deps: bump Pillow 10.4.0 -> 12.3.0 (24 known CVEs); pip pinned
  in Dockerfile and setup_synthid.sh; Docker runs as unprivileged user.
- Resource caps: MAX_INPUT_BYTES 1GiB -> 256MiB, 64MiB stdin cap,
  zip budget 512MiB -> 128MiB, RLIMIT_AS/FSIZE on child processes.
- Tests: 18 new security regression tests (60 total, all passing).
2026-08-12 21:13:53 -07:00
Guillaume Meyer (The Opinionated Man)andGitHub a443019bc8 Merge pull request #6 from CarlosMaeda/fix/harden-cleaners
fix: harden cleaners against argv injection and resource exhaustion
2026-08-12 20:49:43 -07:00
Carlos MaedaandGitHub 4b4c942635 fix: pin reverse-SynthID build and fix Dockerfile ARG scope (#5) 2026-08-12 20:45:07 -07:00
CarlosMaeda a3253d3675 fix: harden cleaners against argv injection and resource exhaustion
Security hardening for the cleaner/inspector entry points:

- common.safe_arg(): prefix './' to paths starting with '-', so a crafted
  filename (e.g. '-@argfile') cannot be interpreted as an option by
  exiftool/c2patool. Applied to every exiftool/c2patool invocation in
  image_meta.py and container_meta.py (clean_pdf).
- container_meta.py: add MAX_ZIP_DECOMPRESSED_BYTES (512 MiB), checked via
  ZipInfo.file_size before decompression in inspect_docx/clean_docx/
  inspect_odt/clean_odt, rejecting zip bombs early.
- clean_file.py / inspect_file.py: refuse inputs larger than
  WATERMARKS_MAX_INPUT_BYTES (default 1 GiB).
- Fix a latent NameError in inspect_docx: the finding used the undefined
  variable `name` instead of info.filename, crashing on the very DOCX files
  this tool exists for.

Tests: tests/test_security_hardening.py covers safe_arg, the zip budget
guard, and the DOCX AI-markers path.
2026-08-12 22:58:15 -03:00
guillaume 25da732ec8 Release v0.3.1: stronger Layer B statistical-watermark rewrite v0.3.1 2026-08-12 17:12:24 -07:00
guillaume 57492ce27b docs: add Star History chart to README 2026-08-12 12:21:53 -07:00
guillaume 545c383201 Release v0.3.0: optional SynthID pixel scoring v0.3.0 2026-08-12 11:16:59 -07:00
guillaume 1693606268 Add optional reverse-SynthID scorer and install paths
- Add score_synthid.py adapter for external reverse-SynthID scoring
- Surface optional SynthID confidence in inspect_image/clean_image
- Add one-command bootstrap script and scorer-only requirements
- Add local Dockerfile for scorer runtime
- Update README, SKILL, vendor notes, Makefile, and tests
2026-08-12 10:37:41 -07:00
guillaume c49592113e Add SynthID research references to README 2026-08-12 10:13:30 -07:00
guillaume 6eb7e9c10a Release v0.2.0: c2patool false-positive fix v0.2.0 2026-08-12 09:06:42 -07:00
raybadteamandGitHub a107b8d105 fix: c2patool "No claim found" reported as a C2PA manifest (#3)
Fixes #1
2026-08-12 09:01:43 -07:00
guillaume 51593cc64c docs: fix broken c2patool links (repo moved to contentauth/c2pa-rs)
The c2patool docs URL (opensource.contentauthenticity.org/docs/c2patool/)
returns 404. Update all four references in README and CONTRIBUTING to the
current repository location. Fixes #2.
2026-08-12 08:50:39 -07:00
guillaume b9c5391445 docs: add disclaimer on the quality cost of text-watermark removal
Layer B removes statistical text marks by rewording, not restructuring,
and every rewrite pass taxes tone, voice, and precision. Document the
trade-off honestly, the full-circle economics of rewriting with a cheaper
model, and when to skip Layer B in favor of the lossless path.
2026-08-12 08:47:28 -07:00
guillaume e1d53c88a5 docs: add v0.1.0 changelog section to README
Complete the release notes that were intended for the v0.1.0 tag.
v0.1.0
2026-08-11 14:28:33 -07:00
guillaume b38ebca404 Release v0.1.0: packaging polish and provenance honesty
Bump latest release pointer and changelog for packaging fixtures,
soft-binding residual-risk docs, and Institute of AI PM reference.
2026-08-11 14:28:11 -07:00
guillaume 481f32aa48 docs: two-layer provenance model and residual-risk honesty
Add Institute of AI PM C2PA/SynthID reference, soft-binding out-of-scope
notes, and residual-risk verification guidance so metadata strip is not
mistaken for full watermark clearance.
2026-08-11 14:08:21 -07:00