unmasker 0.1.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 (173) hide show
  1. unmasker-0.1.0/.editorconfig +17 -0
  2. unmasker-0.1.0/.github/workflows/ci.yml +94 -0
  3. unmasker-0.1.0/.github/workflows/release.yml +70 -0
  4. unmasker-0.1.0/.gitignore +32 -0
  5. unmasker-0.1.0/.pre-commit-config.yaml +22 -0
  6. unmasker-0.1.0/CHANGELOG.md +45 -0
  7. unmasker-0.1.0/CONTRIBUTING.md +183 -0
  8. unmasker-0.1.0/LICENSE +133 -0
  9. unmasker-0.1.0/PKG-INFO +544 -0
  10. unmasker-0.1.0/README.md +510 -0
  11. unmasker-0.1.0/SECURITY.md +66 -0
  12. unmasker-0.1.0/assets/unmasker-banner.svg +36 -0
  13. unmasker-0.1.0/pyproject.toml +83 -0
  14. unmasker-0.1.0/src/unmasker/__init__.py +11 -0
  15. unmasker-0.1.0/src/unmasker/about.py +200 -0
  16. unmasker-0.1.0/src/unmasker/cli.py +220 -0
  17. unmasker-0.1.0/src/unmasker/detect.py +93 -0
  18. unmasker-0.1.0/src/unmasker/findings.py +136 -0
  19. unmasker-0.1.0/src/unmasker/html.py +346 -0
  20. unmasker-0.1.0/src/unmasker/jpeg.py +219 -0
  21. unmasker-0.1.0/src/unmasker/markdown.py +247 -0
  22. unmasker-0.1.0/src/unmasker/metadata/__init__.py +338 -0
  23. unmasker-0.1.0/src/unmasker/metadata/detectors.py +304 -0
  24. unmasker-0.1.0/src/unmasker/metadata/xmp.py +292 -0
  25. unmasker-0.1.0/src/unmasker/odf/__init__.py +1 -0
  26. unmasker-0.1.0/src/unmasker/odf/revisions.py +159 -0
  27. unmasker-0.1.0/src/unmasker/odf/sheets.py +323 -0
  28. unmasker-0.1.0/src/unmasker/odf/slides.py +105 -0
  29. unmasker-0.1.0/src/unmasker/ooxml/__init__.py +1 -0
  30. unmasker-0.1.0/src/unmasker/ooxml/revisions.py +169 -0
  31. unmasker-0.1.0/src/unmasker/ooxml/sheets.py +496 -0
  32. unmasker-0.1.0/src/unmasker/ooxml/slides.py +150 -0
  33. unmasker-0.1.0/src/unmasker/pdf/__init__.py +1 -0
  34. unmasker-0.1.0/src/unmasker/pdf/annotations.py +114 -0
  35. unmasker-0.1.0/src/unmasker/pdf/detectors.py +863 -0
  36. unmasker-0.1.0/src/unmasker/pdf/fonts.py +269 -0
  37. unmasker-0.1.0/src/unmasker/pdf/geometry.py +189 -0
  38. unmasker-0.1.0/src/unmasker/pdf/interpreter.py +853 -0
  39. unmasker-0.1.0/src/unmasker/pdf/rendered.py +177 -0
  40. unmasker-0.1.0/src/unmasker/pdf/tokens.py +344 -0
  41. unmasker-0.1.0/src/unmasker/py.typed +0 -0
  42. unmasker-0.1.0/src/unmasker/readers/__init__.py +73 -0
  43. unmasker-0.1.0/src/unmasker/readers/docx.py +103 -0
  44. unmasker-0.1.0/src/unmasker/readers/image.py +46 -0
  45. unmasker-0.1.0/src/unmasker/readers/model.py +112 -0
  46. unmasker-0.1.0/src/unmasker/readers/odf.py +126 -0
  47. unmasker-0.1.0/src/unmasker/readers/pdf.py +157 -0
  48. unmasker-0.1.0/src/unmasker/readers/plain.py +45 -0
  49. unmasker-0.1.0/src/unmasker/readers/presentation.py +87 -0
  50. unmasker-0.1.0/src/unmasker/readers/spreadsheet.py +166 -0
  51. unmasker-0.1.0/src/unmasker/report.py +242 -0
  52. unmasker-0.1.0/src/unmasker/revisions.py +193 -0
  53. unmasker-0.1.0/src/unmasker/scan.py +165 -0
  54. unmasker-0.1.0/src/unmasker/sheets.py +346 -0
  55. unmasker-0.1.0/src/unmasker/slides.py +133 -0
  56. unmasker-0.1.0/src/unmasker/survey_report.py +215 -0
  57. unmasker-0.1.0/src/unmasker/text/__init__.py +1 -0
  58. unmasker-0.1.0/src/unmasker/text/invisible.py +551 -0
  59. unmasker-0.1.0/src/unmasker/theme.py +117 -0
  60. unmasker-0.1.0/src/unmasker/thumbnails.py +174 -0
  61. unmasker-0.1.0/tests/conftest.py +138 -0
  62. unmasker-0.1.0/tests/specimens/README.md +165 -0
  63. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-hidden-characters.docx +0 -0
  64. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-hidden-characters.md +65 -0
  65. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-metadata-leak.docx +0 -0
  66. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-metadata-leak.md +73 -0
  67. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-tracked-changes.docx +0 -0
  68. unmasker-0.1.0/tests/specimens/docx/libreoffice-writer-tracked-changes.md +88 -0
  69. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-no-thumbnail.jpg +0 -0
  70. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-no-thumbnail.md +39 -0
  71. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-stale-thumbnail.jpg +0 -0
  72. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-stale-thumbnail.md +85 -0
  73. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-thumbnail-regenerated.jpg +0 -0
  74. unmasker-0.1.0/tests/specimens/jpeg/imagemagick-thumbnail-regenerated.md +40 -0
  75. unmasker-0.1.0/tests/specimens/odf/libreoffice-writer-position-note.md +86 -0
  76. unmasker-0.1.0/tests/specimens/odf/libreoffice-writer-position-note.odt +0 -0
  77. unmasker-0.1.0/tests/specimens/odp/libreoffice-impress-hidden-slide.md +53 -0
  78. unmasker-0.1.0/tests/specimens/odp/libreoffice-impress-hidden-slide.odp +0 -0
  79. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-filtered-rows.md +70 -0
  80. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-filtered-rows.ods +0 -0
  81. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-formatted-values.md +59 -0
  82. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-formatted-values.ods +0 -0
  83. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-hidden-columns.md +90 -0
  84. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-hidden-columns.ods +0 -0
  85. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-tracked-changes.md +98 -0
  86. unmasker-0.1.0/tests/specimens/ods/libreoffice-calc-tracked-changes.ods +0 -0
  87. unmasker-0.1.0/tests/specimens/pdf/chrome-print-css-overlay.md +89 -0
  88. unmasker-0.1.0/tests/specimens/pdf/chrome-print-css-overlay.pdf +0 -0
  89. unmasker-0.1.0/tests/specimens/pdf/chrome-transparent-text.md +74 -0
  90. unmasker-0.1.0/tests/specimens/pdf/chrome-transparent-text.pdf +0 -0
  91. unmasker-0.1.0/tests/specimens/pdf/coverage-edge.md +71 -0
  92. unmasker-0.1.0/tests/specimens/pdf/coverage-edge.pdf +0 -0
  93. unmasker-0.1.0/tests/specimens/pdf/flattened-to-image.md +54 -0
  94. unmasker-0.1.0/tests/specimens/pdf/flattened-to-image.pdf +0 -0
  95. unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-clipped-overflow.md +88 -0
  96. unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-clipped-overflow.pdf +0 -0
  97. unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-rotated-headers.md +95 -0
  98. unmasker-0.1.0/tests/specimens/pdf/libreoffice-calc-rotated-headers.pdf +0 -0
  99. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-black-bars.md +87 -0
  100. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-black-bars.pdf +0 -0
  101. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-hidden-in-plain-sight.md +78 -0
  102. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-hidden-in-plain-sight.pdf +0 -0
  103. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-image-over-text.md +68 -0
  104. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-image-over-text.pdf +0 -0
  105. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-metadata-leak.md +45 -0
  106. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-metadata-leak.pdf +0 -0
  107. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-partial-bars.md +79 -0
  108. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-partial-bars.pdf +0 -0
  109. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-pdf-comments.md +77 -0
  110. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-pdf-comments.pdf +0 -0
  111. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-properly-redacted.md +65 -0
  112. unmasker-0.1.0/tests/specimens/pdf/libreoffice-writer-properly-redacted.pdf +0 -0
  113. unmasker-0.1.0/tests/specimens/pdf/redacted-scan-with-ocr.md +82 -0
  114. unmasker-0.1.0/tests/specimens/pdf/redacted-scan-with-ocr.pdf +0 -0
  115. unmasker-0.1.0/tests/specimens/pdf/text-on-an-image.md +73 -0
  116. unmasker-0.1.0/tests/specimens/pdf/text-on-an-image.pdf +0 -0
  117. unmasker-0.1.0/tests/specimens/pdf/xmp-survives-the-scrub.md +108 -0
  118. unmasker-0.1.0/tests/specimens/pdf/xmp-survives-the-scrub.pdf +0 -0
  119. unmasker-0.1.0/tests/specimens/pptx/libreoffice-impress-hidden-slide.md +87 -0
  120. unmasker-0.1.0/tests/specimens/pptx/libreoffice-impress-hidden-slide.pptx +0 -0
  121. unmasker-0.1.0/tests/specimens/sources/build_calc_filtered_rows.py +119 -0
  122. unmasker-0.1.0/tests/specimens/sources/build_calc_formatted_values.py +134 -0
  123. unmasker-0.1.0/tests/specimens/sources/build_calc_hidden_columns.py +192 -0
  124. unmasker-0.1.0/tests/specimens/sources/build_calc_tracked_changes.py +186 -0
  125. unmasker-0.1.0/tests/specimens/sources/build_chrome_print.py +121 -0
  126. unmasker-0.1.0/tests/specimens/sources/build_chrome_transparent_text.py +96 -0
  127. unmasker-0.1.0/tests/specimens/sources/build_clipped_overflow.py +115 -0
  128. unmasker-0.1.0/tests/specimens/sources/build_coverage_edge.py +191 -0
  129. unmasker-0.1.0/tests/specimens/sources/build_docx_hidden_characters.py +121 -0
  130. unmasker-0.1.0/tests/specimens/sources/build_docx_tracked_changes.py +152 -0
  131. unmasker-0.1.0/tests/specimens/sources/build_image_over_text.py +213 -0
  132. unmasker-0.1.0/tests/specimens/sources/build_impress_deck.py +163 -0
  133. unmasker-0.1.0/tests/specimens/sources/build_libreoffice_hidden_in_plain_sight.py +268 -0
  134. unmasker-0.1.0/tests/specimens/sources/build_libreoffice_writer.py +408 -0
  135. unmasker-0.1.0/tests/specimens/sources/build_metadata_leak.py +128 -0
  136. unmasker-0.1.0/tests/specimens/sources/build_odf_document.py +166 -0
  137. unmasker-0.1.0/tests/specimens/sources/build_pdf_with_comments.py +125 -0
  138. unmasker-0.1.0/tests/specimens/sources/build_redacted_scan_with_ocr.py +193 -0
  139. unmasker-0.1.0/tests/specimens/sources/build_rotated_text.py +139 -0
  140. unmasker-0.1.0/tests/specimens/sources/build_stale_thumbnail.py +160 -0
  141. unmasker-0.1.0/tests/specimens/sources/build_text_on_an_image.py +202 -0
  142. unmasker-0.1.0/tests/specimens/sources/build_xmp_survives_the_scrub.py +151 -0
  143. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-formatted-values.md +90 -0
  144. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-formatted-values.xlsx +0 -0
  145. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-hidden-columns.md +93 -0
  146. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-hidden-columns.xlsx +0 -0
  147. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-tracked-changes.md +74 -0
  148. unmasker-0.1.0/tests/specimens/xlsx/libreoffice-calc-tracked-changes.xlsx +0 -0
  149. unmasker-0.1.0/tests/test_about.py +206 -0
  150. unmasker-0.1.0/tests/test_cli.py +446 -0
  151. unmasker-0.1.0/tests/test_documented_detectors.py +216 -0
  152. unmasker-0.1.0/tests/test_html.py +256 -0
  153. unmasker-0.1.0/tests/test_invisible.py +215 -0
  154. unmasker-0.1.0/tests/test_markdown.py +265 -0
  155. unmasker-0.1.0/tests/test_metadata.py +321 -0
  156. unmasker-0.1.0/tests/test_ocr.py +379 -0
  157. unmasker-0.1.0/tests/test_odf.py +226 -0
  158. unmasker-0.1.0/tests/test_ooxml_revisions.py +250 -0
  159. unmasker-0.1.0/tests/test_pdf_annotations.py +175 -0
  160. unmasker-0.1.0/tests/test_pdf_detectors.py +1158 -0
  161. unmasker-0.1.0/tests/test_pdf_fonts.py +163 -0
  162. unmasker-0.1.0/tests/test_pdf_geometry.py +140 -0
  163. unmasker-0.1.0/tests/test_pdf_interpreter.py +323 -0
  164. unmasker-0.1.0/tests/test_pdf_text.py +310 -0
  165. unmasker-0.1.0/tests/test_pdf_tokens.py +245 -0
  166. unmasker-0.1.0/tests/test_readers.py +251 -0
  167. unmasker-0.1.0/tests/test_scan.py +155 -0
  168. unmasker-0.1.0/tests/test_sheet_changes.py +214 -0
  169. unmasker-0.1.0/tests/test_sheets.py +759 -0
  170. unmasker-0.1.0/tests/test_slides.py +329 -0
  171. unmasker-0.1.0/tests/test_survey_report.py +233 -0
  172. unmasker-0.1.0/tests/test_thumbnails.py +300 -0
  173. unmasker-0.1.0/tests/test_xmp.py +340 -0
@@ -0,0 +1,17 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ indent_style = space
8
+ indent_size = 4
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.{md,yml,yaml,json,toml,svg}]
12
+ indent_size = 2
13
+
14
+ # Specimens are artefacts under test. Nothing may normalise their bytes.
15
+ [tests/specimens/**]
16
+ insert_final_newline = false
17
+ trim_trailing_whitespace = false
@@ -0,0 +1,94 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, macos-latest, windows-latest]
16
+ python-version: ["3.10", "3.12", "3.13"]
17
+
18
+ runs-on: ${{ matrix.os }}
19
+
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+ - name: Install
26
+ run: python -m pip install -e ".[dev]"
27
+ # One test shells out to poppler, and it is the one that proves this tool
28
+ # says something the obvious answer does not: `pdftotext` reports no
29
+ # annotations, so a comment carrying "Do not minute this" is invisible to
30
+ # the first thing anybody reaches for. It skips where poppler is absent,
31
+ # so installing it on one platform is the difference between that claim
32
+ # being checked somewhere and checked nowhere.
33
+ - name: Poppler (for the one test that needs it)
34
+ if: matrix.os == 'ubuntu-latest'
35
+ run: sudo apt-get update && sudo apt-get install -y poppler-utils
36
+ # `ruff check`, not `ruff format --check`. This project lints and does
37
+ # not auto-format: the layout of the report, the specimens' XML and the
38
+ # docstrings are placed deliberately, and a formatter run at publication
39
+ # time would rewrite twelve files nobody asked it to. CI asserts the
40
+ # standard the project actually holds, which is the one in README.md.
41
+ - name: Ruff
42
+ run: ruff check .
43
+ # The specimens are the test suite here and they are committed, so this
44
+ # runs the real thing on every platform. `--ocr` is not exercised: it
45
+ # needs ghostscript and tesseract, and `tests/test_ocr.py` skips rather
46
+ # than failing over their absence. Every test that shells out is guarded
47
+ # the same way - the first CI run failed because one of them was not, and
48
+ # nothing on this machine could have told us.
49
+ - name: Tests
50
+ run: pytest
51
+
52
+ types:
53
+ runs-on: ubuntu-latest
54
+
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ - uses: actions/setup-python@v5
58
+ with:
59
+ python-version: "3.12"
60
+ - name: Install
61
+ run: python -m pip install -e ".[dev]"
62
+ - name: Mypy
63
+ run: mypy
64
+
65
+ package:
66
+ runs-on: ubuntu-latest
67
+
68
+ # The tests above run from a checkout, so they prove nothing about the
69
+ # artifact people actually install. A wheel that cannot be built, or that
70
+ # is missing a file, fails only for them and only after publication.
71
+ steps:
72
+ - uses: actions/checkout@v4
73
+ - uses: actions/setup-python@v5
74
+ with:
75
+ python-version: "3.12"
76
+ - name: Build
77
+ run: |
78
+ python -m pip install build twine
79
+ python -m build
80
+ twine check dist/*
81
+ - name: The typing marker has to be inside the wheel
82
+ run: |
83
+ python - <<'CHECK'
84
+ import glob, zipfile
85
+ names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
86
+ assert "unmasker/py.typed" in names, names
87
+ CHECK
88
+ # Installing it and running it is the part that catches an import which
89
+ # type-checks and does not exist at runtime. Mypy passing is not the
90
+ # program running.
91
+ - name: Install what was built and run it
92
+ run: |
93
+ python -m pip install dist/*.whl
94
+ unmasker --version
@@ -0,0 +1,70 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+
15
+ # Trusted Publishing: PyPI verifies this workflow's identity over OIDC, so
16
+ # there is no API token to store, rotate or leak. `id-token: write` is what
17
+ # mints that identity and is the only permission this needs.
18
+ permissions:
19
+ id-token: write
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/setup-python@v5
24
+ with:
25
+ python-version: "3.12"
26
+
27
+ # A tag is a claim about which version is being released, and nothing
28
+ # else checks it. Tagging v0.2.0 against a pyproject that still says
29
+ # 0.1.0 publishes 0.1.0 under a tag that says otherwise, and PyPI will
30
+ # not let that file name be reused once it is wrong.
31
+ - name: The tag has to be the version being built
32
+ run: |
33
+ python - <<'CHECK'
34
+ import os, pathlib, re, sys
35
+
36
+ tag = os.environ["GITHUB_REF_NAME"]
37
+ text = pathlib.Path("pyproject.toml").read_text(encoding="utf-8")
38
+ declared = re.search(r'^version = "([^"]+)"', text, re.MULTILINE).group(1)
39
+
40
+ if tag != f"v{declared}":
41
+ sys.exit(f"tag {tag} does not match pyproject version {declared}")
42
+ print(f"{tag} matches pyproject {declared}")
43
+ CHECK
44
+
45
+ - name: Build
46
+ run: |
47
+ python -m pip install build twine
48
+ python -m build
49
+ twine check dist/*
50
+
51
+ # The same assertion CI makes on every push. It is repeated here because
52
+ # this is the artifact that actually reaches people, and a wheel missing
53
+ # its typing marker is invisible until somebody imports the package.
54
+ - name: The typing marker has to be inside the wheel
55
+ run: |
56
+ python - <<'CHECK'
57
+ import glob, zipfile
58
+ names = zipfile.ZipFile(glob.glob("dist/*.whl")[0]).namelist()
59
+ assert "unmasker/py.typed" in names, names
60
+ CHECK
61
+
62
+ # Install the built wheel in a clean interpreter and run the console
63
+ # script. Mypy passing is not the program running, and neither is a green
64
+ # test suite executed from a checkout.
65
+ - name: Install what was built and run it
66
+ run: |
67
+ python -m pip install dist/*.whl
68
+ unmasker --version
69
+
70
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,32 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .venv/
9
+ venv/
10
+ .vscode/
11
+ .idea/
12
+ .DS_Store
13
+ .coverage
14
+ htmlcov/
15
+
16
+ # Review archives built with `git archive`: a snapshot of the repository stored
17
+ # inside that repository is redundant the day it is made and wrong after it.
18
+ *.zip
19
+
20
+ # Specimens are the test suite here and DO belong in git - see CLAUDE.md.
21
+ # Only genuinely private material is excluded.
22
+ private-samples/
23
+
24
+ # Working notes, not documentation, and the only two files here that are not
25
+ # published. HANDOFF.md is the running log of what was tried and what it cost,
26
+ # and it names files in a sibling checkout that are private. CLAUDE.md is how
27
+ # to work in this repository rather than what the tool does.
28
+ #
29
+ # CONTRIBUTING.md is tracked, deliberately: its rules are each named after the
30
+ # failure that produced them, and that is worth publishing.
31
+ HANDOFF.md
32
+ CLAUDE.md
@@ -0,0 +1,22 @@
1
+ # Lint only. This project does not auto-format: the report layout, the
2
+ # specimens' XML and the docstrings are placed deliberately, and `ruff format`
3
+ # would rewrite a dozen files nobody asked it to. CI runs the same check.
4
+ repos:
5
+ - repo: https://github.com/astral-sh/ruff-pre-commit
6
+ rev: v0.6.9
7
+ hooks:
8
+ - id: ruff
9
+ args: [--fix]
10
+
11
+ # `if False:` is how a watched failure gets parked while the test that proves
12
+ # it is written, and ruff's selected rules (E, F, I, UP, B) do not flag it.
13
+ # One such guard reached the working tree in `jpeg.py` and disabled the
14
+ # start-of-scan check, so `dimensions()` read a frame header out of
15
+ # compressed data. The test caught it; nothing else would have.
16
+ - repo: local
17
+ hooks:
18
+ - id: no-parked-guards
19
+ name: no `if False:` left in src/
20
+ entry: 'if False:'
21
+ language: pygrep
22
+ files: ^src/.*\.py$
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ Notable changes to `unmasker`, newest first. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the versions
5
+ follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ `--json` carries its own `schema` field, which is versioned separately from the
8
+ release: `unmasker.scan/1` for one file, `unmasker.survey/1` for a folder. A
9
+ consumer should read that rather than the release number, because the release
10
+ moves whenever anything does and the schema moves only when the shape changes.
11
+
12
+ ## [0.1.0] - 2026-09-05
13
+
14
+ First release.
15
+
16
+ ### Added
17
+
18
+ - 25 detectors across seven groups: covered and under-image text, invisible and
19
+ low-contrast text, off-page text; zero-width, bidi, tag and mixed-script
20
+ characters; hidden sheets, rows, columns, filtered rows and tracked cell
21
+ changes; hidden slides and speaker notes; stale EXIF thumbnails; tracked
22
+ deletions, comments, revision history and metadata leaks; and — with `--ocr` —
23
+ text that renders but does not extract, and text that extracts but does not
24
+ render.
25
+ - Readers for PDF, DOCX, ODT, XLSX, ODS, PPTX, ODP, JPEG and plain text,
26
+ dispatched by content rather than by extension.
27
+ - Folder surveys: point it at a directory and it reports which files to open
28
+ next, without ranking them against each other.
29
+ - `--html` and `--md` reports, both escaping every value they quote, because
30
+ everything quoted came out of a document somebody else wrote.
31
+ - `--json` for pipelines, with a `schema` field naming the shape and a
32
+ `searched` field distinguishing *nothing was found* from *there was nothing to
33
+ search*.
34
+ - Three exit statuses: `0` clean, `1` findings, `2` unreadable — because a file
35
+ that could not be read is not a file that came back clean.
36
+ - `py.typed`, so the annotations are visible to anything importing `unmasker`
37
+ as a library.
38
+
39
+ ### Fixed
40
+
41
+ - `jpeg.dimensions()` walked past the start-of-scan marker and read a `0xFFC0`
42
+ out of the entropy-coded data, reporting a picture the size of noise. It now
43
+ stops at `SOS`/`EOI` the way `_segments()` already did.
44
+
45
+ [0.1.0]: https://github.com/osint-shifu/unmasker/releases/tag/v0.1.0
@@ -0,0 +1,183 @@
1
+ # Contributing
2
+
3
+ Thank you for looking. This project has a small number of rules and every one
4
+ of them was paid for by a bug. They are worth reading before writing code,
5
+ because a change that breaks one of them will be asked to change back.
6
+
7
+ ## Setting up
8
+
9
+ ```bash
10
+ python3 -m venv .venv
11
+ .venv/bin/pip install -e ".[dev]"
12
+ .venv/bin/pytest
13
+ .venv/bin/ruff check .
14
+ ```
15
+
16
+ Python 3.10 or later. One runtime dependency, `pypdf`.
17
+
18
+ ## The one thing this tool must never do
19
+
20
+ **State something the evidence does not support.** No "this document was
21
+ manipulated". Show what a human sees, show what a machine reads, name the gap,
22
+ and stop.
23
+
24
+ A reader who disagrees with a finding must be able to see why. A tool that
25
+ concludes for its reader is a tool whose conclusion has to be trusted blindly,
26
+ and nobody should trust this one blindly.
27
+
28
+ Everything below follows from that.
29
+
30
+ ## A new detector needs a specimen a real producer wrote
31
+
32
+ This is the rule that matters most, and it is not negotiable.
33
+
34
+ A fixture built from a specification proves that your reader agrees with your
35
+ reading of the spec. It does not prove it agrees with LibreOffice. The sibling
36
+ project `filetrail` had a HEIC reader with a fully green test suite that
37
+ decoded nothing at all on every real file, because every fixture had been built
38
+ from the standard and no real encoder writes files that way.
39
+
40
+ So:
41
+
42
+ 1. Write a **builder** in `tests/specimens/sources/` that drives a real
43
+ producer — `soffice`, `google-chrome`, `gs`, `tesseract`, `exiftool` — to
44
+ write the file.
45
+ 2. Commit the produced file. The specimens **are** the test suite here, which
46
+ is why they are in git rather than gitignored.
47
+ 3. Write a `.md` beside it recording what produced it, what a human sees, what
48
+ is actually in the file, and **what it does not carry**.
49
+
50
+ Everything in a specimen must be invented. No real documents, no real people;
51
+ the e-mail domain is `example.org`, which RFC 2606 reserves for the purpose.
52
+
53
+ If the producer does something the specification does not describe — and it
54
+ will — that is the most valuable thing in your pull request. Write it down.
55
+
56
+ ## Watch the test fail first
57
+
58
+ Write the test, run it, read the failure, *then* implement. A test written
59
+ afterwards passes against broken code, and this project has caught itself
60
+ doing that twice.
61
+
62
+ Check the failure is the one you expected. An `ImportError` is not evidence
63
+ that your test works.
64
+
65
+ ## Break your own rule and see whether the suite notices
66
+
67
+ Before opening a pull request, take each claim your docstrings make, break it
68
+ in the source, and run the suite. Anything that stays green is a claim nothing
69
+ is holding.
70
+
71
+ This has found something every single time it has been run here — most
72
+ recently a guard that could not fail because the fixture feeding it had a
73
+ default that made the question moot. If you find one, the fix is a test, and
74
+ that test is the interesting part of the change.
75
+
76
+ ## Report a line, never a show-operation
77
+
78
+ If you are touching the PDF detectors: producers disagree wildly about how much
79
+ text one drawing operation carries. Chrome writes one glyph per `Tj`; tesseract
80
+ writes one operation per word. Group by **line on the page**, using
81
+ `detectors._lines`, or a single hidden line becomes eighty-seven findings.
82
+
83
+ That rule has been broken five times in this codebase.
84
+ `test_no_detector_reports_per_show_operation` now asks the question directly.
85
+
86
+ ## Do not rank findings against each other
87
+
88
+ A page can have a bar over its text *and* invisible characters *and* stale
89
+ metadata. Those are three findings, not one winner. `filetrail` printed the
90
+ winner once, and a geotagged photograph that had been downloaded reported its
91
+ URL and no GPS at all.
92
+
93
+ Nothing here sorts by strength, and `Finding` carries no score.
94
+
95
+ ## "Nothing found" has two meanings
96
+
97
+ It can mean *searched, and it is not there*. It can mean *there was nothing to
98
+ search*. A reader who confuses those has drawn a conclusion the tool never
99
+ supported.
100
+
101
+ Carry the difference in the data rather than reconstructing it later — a PDF
102
+ with no content stream to parse is not a PDF with nothing hidden, and the
103
+ report has to be able to say which it met.
104
+
105
+ ## The name of a field is evidence
106
+
107
+ `LibreOffice/24.2.7.2$Linux_X86_64` contains a dotted quad. Pattern-matching
108
+ alone reports an IP address — but the field is called `Producer`, so it is a
109
+ version. Every metadata field's meaning is decided by its name **and its
110
+ container**: a PDF's `/Creator` is an application, an OOXML `dc:creator` is a
111
+ person.
112
+
113
+ Context you already have beats a cleverer pattern.
114
+
115
+ ## Colour and words
116
+
117
+ **Colour encodes how the tool knows, never how bad the finding is.** Three
118
+ classes — `direct`, `circumstantial`, `self-reported` — and nothing else in
119
+ the output may use colour.
120
+
121
+ Prefer a word a reader can argue with over a number that implies a precision
122
+ you do not have. `55` reads as a probability; it never was one.
123
+
124
+ ## Modes are commands, not flags
125
+
126
+ If an option ignores most of the other options and is mutually exclusive with
127
+ its neighbours, it is a subcommand. `--json` changes the shape of the output,
128
+ not what the tool did, which is what keeps it a flag.
129
+
130
+ ## Output
131
+
132
+ - **Nothing is truncated.** A value too long for the line wraps. An ellipsis
133
+ sends the reader to fetch the value another way.
134
+ - **Nothing draws past the width it declared.**
135
+ - **Size a column per section, not once per screen.** One global width makes a
136
+ layout look *almost* aligned, which reads worse than not aligning it.
137
+ - A description that restates its flag teaches the reader to skip
138
+ descriptions, and once they skip one they skip the rest.
139
+ - Every command the tool prints must run in the shell that printed it. A screen
140
+ disproved by the first thing a reader tries loses credibility for everything
141
+ else on it.
142
+
143
+ The report's layout and palette are the sibling project `filetrail`'s, and its
144
+ `DESIGN.md` is the design language for both — **read it before inventing a
145
+ second one.** The same goes for anything it already solves: its embedded
146
+ metadata sources read PDF, OOXML, ODF, legacy OLE2 and EXIF, and were read
147
+ before the metadata reader here was written.
148
+
149
+ What is deliberately *not* shared is the identity. `filetrail` is a terminal;
150
+ `unmasker` is the document the terminal is looking at.
151
+
152
+ ## Commits
153
+
154
+ Prose, and no trailers. Say what changed and **why** — the why is the part
155
+ that cannot be recovered from the diff. If a change corrects an earlier
156
+ assumption, say what the assumption was; those are the most useful messages in
157
+ this repository's history.
158
+
159
+ Stage deliberately. `git add -A` once swept a build archive into two commits of
160
+ the sibling project and it reached a public repository.
161
+
162
+ ## What a good pull request looks like
163
+
164
+ - a specimen a producer wrote, with its `.md`
165
+ - a test you watched fail
166
+ - the producer fact you measured, written down where the next person will hit it
167
+ - the gap your change does *not* close, named rather than left implied
168
+
169
+ ## Reporting a bug
170
+
171
+ The most useful report is a file. If it is not one you can share — and in this
172
+ line of work it usually is not — a builder that produces something with the
173
+ same shape is just as good, and better for everyone.
174
+
175
+ If a finding is wrong, say what the tool printed and what the document actually
176
+ shows. A false positive is a bug of the same size as a miss here: a report a
177
+ reader learns to scroll past has stopped working.
178
+
179
+ ## Security
180
+
181
+ See [`SECURITY.md`](SECURITY.md). In short: this tool parses hostile files by
182
+ design, so a crash or a hang on a malformed document is a bug worth reporting
183
+ privately first.
unmasker-0.1.0/LICENSE ADDED
@@ -0,0 +1,133 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction, and
10
+ distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by the
13
+ copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all other
16
+ entities that control, are controlled by, or are under common control with
17
+ that entity. For the purposes of this definition, "control" means (i) the
18
+ power, direct or indirect, to cause the direction or management of such
19
+ entity, whether by contract or otherwise, or (ii) ownership of fifty percent
20
+ (50%) or more of the outstanding shares, or (iii) beneficial ownership of
21
+ such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity exercising
24
+ permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation source,
28
+ and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical transformation
31
+ or translation of a Source form, including but not limited to compiled object
32
+ code, generated documentation, and conversions to other media types.
33
+
34
+ "Work" shall mean the work of authorship, whether in Source or Object form,
35
+ made available under the License, as indicated by a copyright notice that is
36
+ included in or attached to the work.
37
+
38
+ "Derivative Works" shall mean any work, whether in Source or Object form,
39
+ that is based on (or derived from) the Work and for which the editorial
40
+ revisions, annotations, elaborations, or other modifications represent, as a
41
+ whole, an original work of authorship. For the purposes of this License,
42
+ Derivative Works shall not include works that remain separable from, or
43
+ merely link (or bind by name) to the interfaces of, the Work and Derivative
44
+ Works thereof.
45
+
46
+ "Contribution" shall mean any work of authorship, including the original
47
+ version of the Work and any modifications or additions to that Work or
48
+ Derivative Works thereof, that is intentionally submitted to Licensor for
49
+ inclusion in the Work by the copyright owner or by an individual or Legal
50
+ Entity authorized to submit on behalf of the copyright owner.
51
+
52
+ "Contributor" shall mean Licensor and any individual or Legal Entity on
53
+ behalf of whom a Contribution has been received by Licensor and subsequently
54
+ incorporated within the Work.
55
+
56
+ 2. Grant of Copyright License. Subject to the terms and conditions of this
57
+ License, each Contributor hereby grants to You a perpetual, worldwide,
58
+ non-exclusive, no-charge, royalty-free, irrevocable copyright license to
59
+ reproduce, prepare Derivative Works of, publicly display, publicly perform,
60
+ sublicense, and distribute the Work and such Derivative Works in Source or
61
+ Object form.
62
+
63
+ 3. Grant of Patent License. Subject to the terms and conditions of this
64
+ License, each Contributor hereby grants to You a perpetual, worldwide,
65
+ non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this
66
+ section) patent license to make, have made, use, offer to sell, sell, import,
67
+ and otherwise transfer the Work, where such license applies only to those
68
+ patent claims licensable by such Contributor that are necessarily infringed
69
+ by their Contribution(s) alone or by combination of their Contribution(s)
70
+ with the Work to which such Contribution(s) was submitted.
71
+
72
+ If You institute patent litigation against any entity (including a
73
+ cross-claim or counterclaim in a lawsuit) alleging that the Work or a
74
+ Contribution incorporated within the Work constitutes direct or contributory
75
+ patent infringement, then any patent licenses granted to You under this
76
+ License for that Work shall terminate as of the date such litigation is filed.
77
+
78
+ 4. Redistribution. You may reproduce and distribute copies of the Work or
79
+ Derivative Works thereof in any medium, with or without modifications, and
80
+ in Source or Object form, provided that You meet the following conditions:
81
+
82
+ (a) You must give any other recipients of the Work or Derivative Works a
83
+ copy of this License; and
84
+
85
+ (b) You must cause any modified files to carry prominent notices stating
86
+ that You changed the files; and
87
+
88
+ (c) You must retain, in the Source form of any Derivative Works that You
89
+ distribute, all copyright, patent, trademark, and attribution notices from
90
+ the Source form of the Work, excluding those notices that do not pertain to
91
+ any part of the Derivative Works; and
92
+
93
+ (d) If the Work includes a "NOTICE" text file as part of its distribution,
94
+ then any Derivative Works that You distribute must include a readable copy
95
+ of the attribution notices contained within such NOTICE file, excluding those
96
+ notices that do not pertain to any part of the Derivative Works, in at least
97
+ one of the following places: within a NOTICE text file distributed as part of
98
+ the Derivative Works; within the Source form or documentation, if provided
99
+ along with the Derivative Works; or, within a display generated by the
100
+ Derivative Works, if and wherever such third-party notices normally appear.
101
+
102
+ 5. Submission of Contributions. Unless You explicitly state otherwise, any
103
+ Contribution intentionally submitted for inclusion in the Work by You to the
104
+ Licensor shall be under the terms and conditions of this License, without
105
+ any additional terms or conditions.
106
+
107
+ 6. Trademarks. This License does not grant permission to use the trade names,
108
+ trademarks, service marks, or product names of the Licensor, except as
109
+ required for reasonable and customary use in describing the origin of the
110
+ Work and reproducing the content of the NOTICE file.
111
+
112
+ 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
113
+ writing, Licensor provides the Work (and each Contributor provides its
114
+ Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
115
+ KIND, either express or implied, including, without limitation, any warranties
116
+ or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
117
+ PARTICULAR PURPOSE.
118
+
119
+ 8. Limitation of Liability. In no event and under no legal theory, whether in
120
+ tort (including negligence), contract, or otherwise, unless required by
121
+ applicable law, shall any Contributor be liable to You for damages, including
122
+ any direct, indirect, special, incidental, or consequential damages of any
123
+ character arising as a result of this License or out of the use or inability
124
+ to use the Work.
125
+
126
+ 9. Accepting Warranty or Additional Liability. While redistributing the Work
127
+ or Derivative Works thereof, You may choose to offer, and charge a fee for,
128
+ acceptance of support, warranty, indemnity, or other liability obligations
129
+ and/or rights consistent with this License. However, in accepting such
130
+ obligations, You may act only on Your own behalf and on Your sole
131
+ responsibility, not on behalf of any other Contributor.
132
+
133
+ END OF TERMS AND CONDITIONS