Metadata-Version: 2.5
Name: xlsx-provenance
Version: 1.0.0
Summary: Fingerprint .xlsx files for authorship and authenticity: Excel vs openpyxl, xlsxwriter, Aspose, LibreOffice and others
Project-URL: Homepage, https://github.com/jtannahill/xlsx-provenance
Project-URL: Repository, https://github.com/jtannahill/xlsx-provenance
Project-URL: Issues, https://github.com/jtannahill/xlsx-provenance/issues
Author: James Tannahill
License-Expression: MIT
License-File: LICENSE
Keywords: excel,forensics,metadata,ooxml,openpyxl,provenance,xlsx
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
Classifier: Topic :: Security
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# xlsx-provenance

![Status](https://img.shields.io/badge/status-active-success)
![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white)
![License](https://img.shields.io/github/license/jtannahill/xlsx-provenance)
![Last Commit](https://img.shields.io/github/last-commit/jtannahill/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

```bash
git clone https://github.com/jtannahill/xlsx-provenance.git ~/xlsx-provenance
ln -s ~/xlsx-provenance/xlsx-provenance ~/bin/xlsx-provenance
# ensure ~/bin is on PATH
```

Or just run the script directly:

```bash
~/xlsx-provenance/xlsx-provenance some-file.xlsx
```

## Usage

```bash
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 of `HeadingPairs` + `TitlesOfParts` (Excel-only, openpyxl skips)
- **`docProps/core.xml`**: `creator`, `lastModifiedBy`, `created` / `modified` timestamps (and their delta; < 1s smells automated), `lastPrinted`
- **`xl/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.xml` size (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 *omits* `count="N"` on `<cellXfs>`; openpyxl always includes it. Excel writes `<tableStyles>`, `<indexedColors>`.
- **`xl/sharedStrings.xml`**: `uniqueCount` attribute 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:

```bash
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
