xlsx-provenance
Fingerprint .xlsx files for authorship and authenticity. Tells you whether a workbook was actually authored in Excel, or generated by a library (openpyxl, xlsxwriter, Aspose, ClosedXML, EPPlus, SheetJS, ...) or another office suite (LibreOffice, OnlyOffice, WPS, ...).
Stdlib-only Python. No third-party dependencies.
Why
An .xlsx is a zip of XML. Every authoring tool leaves fingerprints: different Application strings, different fileVersion attributes, different presence/absence of calcChain.xml, theme1.xml, printerSettings*.bin, <HeadingPairs>, <TitlesOfParts>, cellXfs count="" attributes, and so on.
This tool reads those fingerprints and gives you a per-file verdict you can act on.
Install
pipx install xlsx-provenance # or: uv tool install xlsx-provenance
brew install jtannahill/tap/xlsx-provenance
Or run straight from a checkout (no install, stdlib only):
git clone https://github.com/jtannahill/xlsx-provenance.git
./xlsx-provenance/xlsx-provenance some-file.xlsx
Editing metadata
The same tool can rewrite the declared metadata, either to scrub a file before sharing it or to set the properties you want.
xlsx-provenance --strip report.xlsx # blank author, company, dates, application... in place
xlsx-provenance --strip --backup report.xlsx # same, keeping report.xlsx.bak
xlsx-provenance --strip -o clean.xlsx report.xlsx # write to a new file
xlsx-provenance --set creator="Jane Doe" --set company=Acme --set created=2024-01-02 report.xlsx
xlsx-provenance --strip --set creator="Jane Doe" report.xlsx # scrub, then set just one
xlsx-provenance --list-properties # keys accepted by --set
Only docProps/core.xml and docProps/app.xml are rewritten. Every other zip
member is copied byte for byte in its original order, so the workbook stays
exactly as valid as it was and the structural provenance signals are untouched.
An empty value (--set manager=) removes a property; dates take ISO 8601 and
are stored as UTC.
This edits declared metadata. It is not a way to pass a generated file off as
Excel-authored: the analyzer requires the workbook body to corroborate a
declared Excel application (fileVersion, theme, styles, calcChain...), and a
file that claims Excel without that structure is reported as SUSPECT. A
stripped file is reported with a metadata=stripped signal.
Usage
xlsx-provenance file1.xlsx [file2.xlsx ...]
xlsx-provenance *.xlsx
xlsx-provenance --json file.xlsx # machine-readable
xlsx-provenance -v file.xlsx # show full signal breakdown
xlsx-provenance -q *.xlsx # one line per file
xlsx-provenance --no-color file.xlsx # plain output
Exit code is 0 if every file is verdict EXCEL_* or a non-Excel office suite (LIBREOFFICE, ONLYOFFICE, etc.). It is 1 if any file came back from a programmatic library (OPENPYXL, XLSXWRITER, ...) or as SUSPECT / UNKNOWN / MISSING / INVALID. Useful in CI.
Verdicts
| Verdict | Meaning |
|---|---|
EXCEL_MAC |
Microsoft Macintosh Excel. Authentic Excel for Mac |
EXCEL_WIN |
Microsoft Excel. Authentic Excel for Windows or Online |
EXCEL_OTHER |
Some other Excel variant string, but Excel fileVersion confirmed |
EXCEL_LIKELY |
No explicit Application, but enough Excel-only artifacts (calcChain, fileVersion appName=xl, real theme, printerSettings, VBA, threaded comments, etc.) to be confident |
OPENPYXL, XLSXWRITER, ASPOSE, CLOSEDXML, EPPLUS, OPENXML_SDK, SHEETJS, SPREADJS, SYNCFUSION, GEMBOX, SPIRE, LUCKYSHEET, PYTHON_XLSX |
Application string explicitly declared a generation library |
LIBREOFFICE, ONLYOFFICE, OPENOFFICE, GNUMERIC, CALLIGRA, WPS_OFFICE, APPLE_NUMBERS, GOOGLE_SHEETS |
Authentic but non-Excel office suite |
SUSPECT |
No Application and no fileVersion. Looks tampered or hand-built |
UNKNOWN |
Couldn't classify |
MISSING |
File doesn't exist |
INVALID |
Not a valid zip / corrupted |
Signals examined
docProps/app.xml:Application,AppVersion,Company,Manager,DocSecurity, presence ofHeadingPairs+TitlesOfParts(Excel-only, openpyxl skips)docProps/core.xml:creator,lastModifiedBy,created/modifiedtimestamps (and their delta; < 1s smells automated),lastPrintedxl/workbook.xml:<fileVersion appName="xl" rupBuild="...">(Excel-only),workbookPr/@codeName, defined names, sheet count- Zip artifacts:
calcChain.xml(Excel writes, libraries usually skip),theme/theme1.xmlsize (Excel: ~6796–8390 B; openpyxl: < 4 KB),printerSettings*.bin,vbaProject.bin,pivotTables/,pivotCache/,connections.xml,externalLinks/,charts/,drawings/,comments*.xml,threadedComments(Excel 365),tables/,queryTables/ xl/styles.xml: Excel often omitscount="N"on<cellXfs>; openpyxl always includes it. Excel writes<tableStyles>,<indexedColors>.xl/sharedStrings.xml:uniqueCountattribute presence[Content_Types].xml: number of overrides, presence of theme override
The verdict combines a hard match on the Application string with a soft score (0–14) over the structural signals. High score with no library declaration → EXCEL_LIKELY.
Examples
Pretty (default):
=== model.xlsx ===
[EXCEL_MAC] Excel signal score 11/14 (confidence: high)
application Microsoft Macintosh Excel / AppVersion 16.0300
identity creator='' lastModifiedBy='james tannahill'
fileVersion appName='xl' lastEdited='7' lowestEdited='7' rupBuild='10503'
workbook sheets=11 definedNames=0 codeName=None
artifacts calcChain, theme1(6798B), printerSettings, drawings
styles.xml count attr omitted, tableStyles, indexedColors
content-types 18 overrides, theme=True
zip 22 entries
Library tell:
=== generated.xlsx ===
[OPENPYXL] Application explicitly declares openpyxl (confidence: high)
application Microsoft Excel Compatible / Openpyxl 3.1.5 / AppVersion 3.1
identity creator='' lastModifiedBy='someone'
fileVersion <missing>
...
JSON for piping into other tooling:
xlsx-provenance --json *.xlsx | jq '.[] | select(.verdict == "OPENPYXL") | .path'
How to "fix" an openpyxl-generated file
Open it in real Excel and File → Save As (overwrite or new name). Excel rewrites every metadata field (Application, fileVersion, calcChain, theme) to its native fingerprint. Editing docProps/app.xml by hand only fixes the visible Application string and leaves the deeper structural tells intact.
License
MIT
Metadata
Release files for xlsx-provenance 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xlsx_provenance-1.1.0.tar.gz | 14.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xlsx_provenance-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 28.7 kB
Release files / xlsx_provenance-1.1.0.tar.gz
| Download URL | xlsx_provenance-1.1.0.tar.gz |
|---|---|
| Size | 14.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b12f4fb06ff2b10a72f6a2f519b01bd0b3f36a2396108b3fab05ad551b9bc31f
|
|
BLAKE2b-256 checksum How to use checksums |
6e809a9b804f213db55e8900ad4b766e7d9510ac3a80674bbf5703a56214b7c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / xlsx_provenance-1.1.0-py3-none-any.whl
| Download URL | xlsx_provenance-1.1.0-py3-none-any.whl |
|---|---|
| Size | 14.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
930b42560cb67e4ffcef97ac360754d0611cc1cbd1185021d70b3e6341d039fd
|
|
BLAKE2b-256 checksum How to use checksums |
eb4e7cfbce1d2dca47976bc87743961798b46ef5a12270524850939940f2e257
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|