Skip to content

Add an edit timeline and a summarised PDF report - #5

Merged
overjoyde merged 10 commits into
overjoyde:mainfrom
pepo72:feat/edit-timeline
Sep 26, 2026
Merged

overjoyde merged 10 commits into
overjoyde:mainfrom
pepo72:feat/edit-timeline

Conversation

@pepo72

@pepo72 pepo72 commented Sep 26, 2026

Copy link
Copy Markdown

Summary

This adds an edit timeline and a summarised PDF report. For a PDF or Word document it shows what was edited, when, and what it was changed to, and --pdf-report writes the result as a PDF.

This branch builds on #4 (document timestamps are validated there). Until #4 is merged, the diff below also shows its two commits; the commits for this feature start at "Record signing and timestamp times from pyHanko".

What the timeline shows

Each row has a time, the source of that time, what changed, and before → after. For examples/bank-statement/03_edited_in_acrobat.pdf:

When Time source What changed Before → after
2026-02-01 06:12:40 +01:00 Info /CreationDate of revision 1 (claimed) Original version
2026-09-18 21:47:05 +02:00 Info /ModDate of revision 2 (claimed) Text on page 1 changed in revision 2 …Lön Exempelföretaget AB 32 450,00 36 602,52 → …52 450,00 56 602,52, and five more lines

Sources: the text diff between revisions (already computed), the save time each revision wrote into Info or XMP, signature and document-timestamp times from pyHanko, XMP history, and Word tracked changes (author, w:date, text).

How much a time proves

Every time is labelled with what backs it, because a time inside a file is only as good as its source:

  • claimed: written by the editing software (Info /ModDate, XMP, signature /M, w:date). Anyone who edits the file can change it.
  • signed: the signingTime attribute inside a signature that matches the file.
  • timestamped: an RFC 3161 token that matches the file. The report says the issuer is not verified, because trust roots are not configured.

A revision that did not write its own save time gets none, rather than inheriting the previous one. Times from a signature that does not match the file drop to claimed.

Ordering

PDF rows follow the order of the revisions, which the file's bytes fix, and are sorted by time within a revision. Sorting purely by claimed time would let a backdated edit appear before the signature it follows. When the claimed times contradict the revision order, a new finding timeline.inconsistent-times (MEDIUM) is raised. 05_signed_then_edited.pdf now gets this finding: revision 3 claims 2026-09-18 but comes after a signature made on 2026-09-26. Its verdict is unchanged. If you prefer pure time order, it is a small change in timeline._sort.

Changes

  • timeline.py (new): builds the events from other analysers' facts and runs through collect(), so a failure marks the analysis incomplete.
  • pdfreport.py (new): the PDF report with reportlab. It contains the verdict, the key findings, the timeline table, what was checked, limitations and method. reportlab is imported only when a report is written.
  • revisions.py: records the save time each revision claims. signatures.py: records signing and timestamp times. office.py: keeps each Word tracked change with author, date and text.
  • cli.py: --pdf-report PATH, which takes a directory when several files are analysed. Without reportlab it exits 2 with an install hint.
  • pyproject.toml: new extra report = ["reportlab>=4.0"]. README: options, extras, licenses, findings.

Excel and PowerPoint are not covered; the report says so.

Testing

  • pytest: all tests pass, also with -W error::DeprecationWarning. There are new tests for each source of time, the evidence labels (including broken signatures and a missing signingTime attribute), revision ordering with a backdated edit, timezone-naive dates, Word changes split over formatting runs, a revision that could not be reconstructed, and the PDF report. The report tests cover hostile markup, characters outside Windows-1252, a fully rewritten page, very long unbroken lines, a missing timeline, spreadsheets, batch output and the case without reportlab.
  • Verdicts and finding IDs for all bank-statement examples are unchanged; 05 additionally gets timeline.inconsistent-times.
  • The reports for 03 and 05 were rendered and checked visually. The tables wrap inside the page, å, ä and ö render, and the header repeats on each page.

Signatures
- Load each signature separately. pyHanko's embedded_signatures raised
  on the first unreadable signature container, which left every
  signature in the file unvalidated and only produced a LOW note that
  blamed a legacy format.
- An unreadable signature container is now signature.unparseable (HIGH).
  A pyHanko failure while reading the signatures marks the analysis
  incomplete instead of only being recorded in the facts.
- signature.validation-error keeps MEDIUM effective severity (confidence
  MEDIUM instead of LOW): integrity is unknown, which needs review.
- pdfsig reporting that it did not verify a signature is now
  pdfsig.integrity-unknown instead of producing no finding.

Summary, batch report and exit code
- "Checked and found in order" lines are only written for analysers
  that ran and reported no error, for PDF and Office alike, and the
  signature line is withheld when any signature finding leaves
  integrity in doubt.
- The batch report marks incomplete verdicts and counts them apart
  from documents without significant findings.
- --fail-on also exits 1 when an analysis is incomplete or a file
  could not be analysed.
…s, and make --fail-on-incomplete opt-in

Follow-up after review:

- Document timestamps (/DocTimeStamp) are validated with
  validate_pdf_timestamp instead of failing validate_pdf_signature, so
  PAdES-LTA and other timestamped files are no longer flagged.
- pdfsig: an unsigned signature field is skipped, and "integrity
  unknown" is only reported when pyHanko did not settle the integrity
  of that signature either (Poppler does not verify document
  timestamps).
- pyHanko decrypts files that have only an owner password (empty user
  password), or uses --password, before reading the signatures.
- signature.unparseable is only used when the CMS container itself
  cannot be read; other loader failures are validation errors.
  signature.not-validated is not added when pyHanko could not read the
  file at all, since the analysis is then already incomplete.
- Field names are stored as plain strings (pyHanko returns proxy
  objects for encrypted files, which broke report serialisation).
- --fail-on keeps its previous behaviour. The new --fail-on-incomplete
  exits 1 when an analysis is incomplete or a file could not be
  analysed. The batch summary lists incomplete files in its JSON.
…rouping, layout

- A revision that did not write /Info or XMP gets no save time instead
  of inheriting the previous revision's, and the consistency check only
  compares times revisions wrote themselves.
- Times from a signature or timestamp that does not match the file are
  only "claimed". Only pyHanko's signed signingTime attribute counts as
  signed; a /M fallback stays claimed. RFC 3161 tokens are labelled
  "issuer not verified by this tool", since trust roots are not
  configured.
- Consecutive Word tracked changes by the same author at the same time
  are one change, so text split over formatting runs reads correctly.
- Timezone-naive dates are compared as UTC with a 14-hour tolerance
  instead of being skipped, so a backdate without an offset is flagged.
- Spreadsheets and presentations are marked as not covered, and the PDF
  report says so instead of "no edits found".
- Long changes are split over several table rows by text length, so a
  fully rewritten page no longer makes report generation fail.
- --pdf-report treats the path as a directory whenever several files
  were given, even if only one could be analysed.
@overjoyde
overjoyde merged commit 8e710e1 into overjoyde:main Sep 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants