Skip to content

Free tool

workbook-scan — what a spreadsheet actually does, read out of the file

A workbook a business runs on is undocumented software. workbook-scan is a free, read-only audit tool that opens the .xlsx as what it is — a zip of XML — and reports eleven things that are in the bytes: formulas saved holding an error, links into files that may not exist any more, saved queries to one person's mapped drive, approximate VLOOKUPs, hidden sheets, rules nothing protects. It also has three ways of saying it cannot read the file at all, which is the honest answer often enough to be worth naming. One Python file, standard library only, and nothing in it that writes to anything or opens a socket.

  • Excel .xlsx and .xls
  • 11 rules
  • JSON out
  • Exit 1 on findings
  • Read-only
  • MIT

Install and run

There is no install. One file, standard library only — zipfile, xml.etree and struct, all three in the Python already on the machine. No pip, no openpyxl, no virtual environment. It was written and run here on Python 3.12, which is the only version it has been executed on.

# download it next to wherever the workbook is
curl -O https://plantroomlabs.com/tools/workbook-scan.py

# what does this sheet actually do?
python3 workbook-scan.py pricing-model.xlsx

# several at once, or a whole directory
python3 workbook-scan.py *.xlsx

# the same findings as JSON, for a build or a ticket
python3 workbook-scan.py --json pricing-model.xlsx

It exits 1 when it finds something and 0 when it does not, which is the convention ede-check follows and the reason either can sit in a build step beside a linter. Nothing is uploaded and nothing is written: the workbook is opened for reading and the only output is the report.

What one run looks like

The repository ships two workbooks it generates itself — one deliberately broken, one clean — so the output can be read before anybody sends a real file. This is the broken one, and the run below is pasted by the script that publishes this page rather than retyped.

$ python3 workbook-scan.py fixtures/dirty.xlsx
======================================================================
dirty.xlsx  3503 bytes, 3 sheets, 9 cells, 7 formulas
======================================================================
  cached-error           2
  hidden-sheet           1
  defined-name-broken    1
  external-link          1
  data-connection        1
  volatile               1
  fragile-reference      1
  approximate-lookup     1
  unprotected-formulas   1

  hidden-sheet  Old rates
    the sheet is marked hidden, so it does not appear in the tab bar and its rules are invisible to whoever uses the book
  defined-name-broken  margin
    the named range points at #REF!, so every formula using the name is already wrong
  external-link  /old-server/finance/rates-2014.xlsx
    the workbook reads cells out of another file, so the answer depends on a file that may not exist any more
  data-connection  RatesTemplate -> N:\_Templates\rates\RatesTemplate.xml
    the workbook carries a saved query to a path on one machine - a mapped drive or a share - so it refreshes for whoever set it up and for nobody else
  volatile  Pricing!B4
    the formula uses a volatile function, so the sheet answers differently tomorrow with no input changed
  fragile-reference  Pricing!B5
    the formula uses INDIRECT or OFFSET, which breaks silently when a row is inserted
  approximate-lookup  Pricing!B6
    VLOOKUP is set to approximate match, so on unsorted data it returns a neighbouring row instead of no answer
  cached-error  Pricing
    1 cell in this sheet was saved holding #REF!, so the book was last used in that state
  unprotected-formulas  Pricing
    the sheet carries formulas and has no protection, so anyone can type over a rule and nothing says so
$ echo $?
1

Every line names a sheet, a cell or a path, because “this workbook has problems” is not a finding and cannot be acted on. The clean fixture prints nothing this scanner knows how to find and exits 0.

What three of those rules actually catch

A cached error is not cosmetic

When a formula's last computed value was #REF!, Excel writes the error into the file and types the cell e. The book was saved in that state, so somebody has been looking at a hole in it since. This program was written after finding exactly that in a published engineering calculator: the broken cell was the check the calculator exists to perform, and the file's last revision was nine years old. Nobody had asked whether it still worked, because nothing asks.

An approximate VLOOKUP is the format's most expensive default

VLOOKUP(x, range, col) with the fourth argument left off, or set to TRUE, does not answer “not found” on unsorted data. It returns a neighbouring row. Detecting it needs real argument counting rather than counting commas — a lookup whose third argument is itself a MATCH(...) call has four commas and three arguments — so the scanner splits at the top level only.

A saved data connection refreshes for one person

An external link reads cells out of another file. A data connection is different: it re-runs a query, and when its path is a mapped drive or a share it refreshes for whoever built it and silently for nobody else. Both are in the bytes, with the path, and the path is usually the whole explanation.

It reads the old binary .xls too, and says what it cannot do there

A pre-2007 .xls is not a zip of XML: it is an OLE compound file holding a stream of BIFF records, and both layers are parsed here. Hidden sheets, cached errors, unprotected formulas, external links and VBA all work on one, and being a .xls at all is itself reported — no browser-based tool opens one and every reader of the format is working from a reverse-engineered layout.

$ python3 workbook-scan.py fixtures/dirty.xls
======================================================================
dirty.xls  2048 bytes, 2 sheets, 1 cell, 1 formula
======================================================================
  legacy-binary          1
  hidden-sheet           1
  cached-error           1
  unprotected-formulas   1

  legacy-binary  dirty.xls
    this is the pre-2007 binary .xls format, so no browser-based tool opens it, newer Excel warns before it will, and every reader of it is working from a reverse engineered layout
  hidden-sheet  Old rates
    the sheet is marked hidden, so it does not appear in the tab bar and its rules are invisible to whoever uses the book
  cached-error  Prices
    1 cell in this sheet was saved holding #REF!, so the book was last used in that state
  unprotected-formulas  Prices
    the sheet carries 1 formula and has no protection, so anyone can type over a rule and nothing says so
$ echo $?
1

Three rules are missing from that output on purpose. Volatile functions, INDIRECT and OFFSET, and approximate lookups all need the formula text, and BIFF stores formulas as a token stream keyed by function index. Guessing those indices would mean reporting findings the tool cannot stand behind, so on a .xls it does not claim them rather than claiming them badly.

What it does not tell you

It reports what is in the file, not whether it is a defect. A volatile function is correct in a sheet meant to answer differently today; an unprotected formula is fine in a workbook one person uses; a hidden sheet can be deliberate. The findings are the places worth a question, and the question is for somebody who knows what the sheet is for.

It also reads only what the format stores. A rule that is wrong in a way the file cannot express — a factor typed into the wrong cell, a threshold nobody updated — looks exactly like a correct one from here. That part is the audit, and it is a person reading formulas, not a program.

It has been run on the two fixtures above, on their .xls twin, and on a handful of real published workbooks — which is where the cached-error finding came from. It has never been run against a thousand-sheet model, and a workbook that large may well find a case this parser handles badly. The output names the rule and the cell, so it is checkable rather than something to take on trust.

The repository

The same file, MIT licensed, at github.com/UsamaIqbal0304/workbook-scan. It ships the fixture generator as well as the fixtures, so the broken workbook scanned above can be rebuilt from scratch rather than trusted. Issues and pull requests are read.

The file you are downloading

Published here so the download is checkable rather than trusted. Both figures are read off the file served at /tools/workbook-scan.py when this page is built, so they cannot disagree with it.

PropertyValue
Fileworkbook-scan.py
Size24,561 bytes
SHA-256 ac8a1be0e8d93b9b64c085e2260c51e358bb50da283776f8246d1aa7f4a71a9d
Licence MIT — LICENSE.txt
Source github.com/UsamaIqbal0304/workbook-scan

To check it, on Linux sha256sum workbook-scan.py, on macOS shasum -a 256 workbook-scan.py, on Windows certutil -hashfile workbook-scan.py SHA256. A different digest means a different file — not necessarily a hostile one, but not this one.

The repository holds the same file, byte for byte, together with everything needed to re-run the checks this page's claims rest on — so they can be run rather than read about. Issues and pull requests there are read.

Also free

The others

Same idea, a different protocol or a different file. Every free tool.

Next step

Send the workbook, or the workbook with the numbers blanked.

The rules are in the formulas, so blanking the values costs the read nothing. What comes back is what the sheet encodes and which parts of it disagree with each other - in writing, whether or not anything gets built afterwards.