There is a kind of car you find in a barn under a tarpaulin: forty years old, the right shape, the engine seized, and the previous owner's receipts still in the glovebox. You can tell within a minute whether it is worth the trouble. Most of the time it is not. Now and then the receipts show that somebody cared for it properly until the day they stopped, and then it is.

On Saturday I found one of those, except it was a security tool.

What it is

TIGER was written at Texas A&M University in 1993. Its job was to look over a Unix machine and point at the things an attacker would use: accounts with no password, files anyone could write to, setuid programs that should not be setuid, services listening that nobody meant to start. It is a set of shell scripts. No compiler, no runtime, nothing to install beyond a shell and the usual Unix tools. It belongs to the same generation as COPS and SATAN, which is to say the generation that invented the idea of auditing your own machine before somebody else did.

A Debian developer, Javier Fernández-Sanguino, kept it going from 2001. The last proper release, 3.2.3, was in 2008. He started a 3.2.4 release candidate in 2018 and the repository has been silent since August 2019. The official project still lives on GNU Savannah under his care; what follows is about my fork, which I have named Tigris, and nothing in it is a complaint about him.

What caught my eye was that Debian still ships it. In the seven years since the last commit, five different Debian developers had uploaded fixes to the package so that it would keep building, with no maintainer to send them to. That is the receipts in the glovebox. Somebody cared.

Why bother

I build Wegweiser, a device intelligence platform for managed service providers, and its Linux agents run Lynis on every endpoint. Lynis is good and well maintained. But there are two things TIGER did in 1993 that Lynis does not do today: it checks every installed file against what the package manager says should be there, and when it runs from cron it reports only what has changed since last time. Those are exactly the two things an MSP wants from an auditor. A noisy report every week is ignored by the second week. A report that says "three files changed since Tuesday" gets read.

So the question was not whether TIGER was good. It was whether thirty-three-year-old shell could be made to tell the truth about a 2026 machine.

The first run

I checked the source out, made the tree root's, and ran it on my own desktop: Linux Mint 22.3 on a kernel numbered 7.0. The report came back at 49,651 lines.

That number is not a measure of how insecure my desktop is. It is a measure of how much the world moved while the code stood still. Three things had gone wrong, and each is a small lesson in what time does to software.

Nothing had actually run. Of the twenty-four checks the configuration asked for, twenty-four were skipped. TIGER, sensibly, refuses to run a check script owned by anyone other than the user running it, because root executing a file that an ordinary user can edit is a bad idea. My checkout was owned by me and I had run it with sudo. Every check printed one line of refusal and moved on, and the report had nothing in it but refusals and one enormous section I will come to. The fix was not to relax the rule. It was to check once, up front, and stop with a sentence telling you to chown the tree, instead of producing an empty report that looks like a clean bill of health.

The one check that did run was lying. The section that made up most of those 49,651 lines was a list of 16,114 "dangling symlinks". A dangling symlink is a link that points at a file that is not there, and in a system directory it can matter: a privileged program that follows one into a path somebody else can create is a classic way in. So the check is worth having. But when I looked, 15,517 of the warnings had no path in them at all. They said, literally, cannot access is a dangling symlink.

The cause was lovely. In 1993, find could not tell you who owned a file. So TIGER piped every path on the disk through a loop that ran ls on each one and parsed the output, and it spotted dangling links by catching the text of ls's error message. Thirty years later, coreutils changed how it quotes the file name in that message, the pattern stopped matching, and the warning kept the error and lost the path. Nobody noticed, because nobody had run it in years.

The checks that should have run were the wrong ones. This was the one I enjoyed most. TIGER keeps a directory of checks per operating system and per major version: systems/Linux/2, with symlinks 3, 4, 5 and 6 pointing back at it, because the Linux checks have not needed to change by version. My kernel is version 7. There was no symlink for 7, so the configuration fell back to a default directory, which was the right thing to do, and printed a line saying so. But the variable that the rest of the program uses to find its checks was still set to 7, so every lookup missed, and TIGER quietly ran its generic Unix checks instead of its Linux ones. The listening-process check, for instance, parsed the human-readable output of lsof, where the command name is cut to nine characters and every thread gets its own row, and produced 284 warnings about a process called NetworkMa.

One line fixed it: when you fall back to default, say so in the variable too. Until that line, any kernel newer than the newest symlink turned the Linux-specific half of the tool off without telling anyone.

What time does to software

None of those three bugs was in the logic. The password check still knows what a weak account looks like. The setuid check still knows a setuid binary when it sees one. What had rotted was the seam between the program and the system: the exact wording of an error message, the number in a kernel version, a convention about file ownership that had been obvious in 1993 and is a surprise in 2026 when the natural way to try a tool is git clone and sudo.

That is the general lesson of barn finds, I think. The engine is usually fine. It is the rubber that has perished: the hoses, the seals, the places where one part meets another.

And the thing about shell is that it does not refuse to start. A compiled program from 1993 would not have built. A Python 2 script would have died on the first print statement. TIGER ran, every time, and produced output every time, which is both why it survived and why nobody saw that it was broken. Software that fails loudly gets fixed. Software that fails politely gets a cron entry and is forgotten.

The million processes

The filesystem scan is the heaviest thing TIGER does: walk every local disk looking for setuid files, device nodes, world-writable directories, files with no owner. On my machine that was taking five and a half minutes and 316 seconds of system time. The reason was the same 1993 loop: for every one of several hundred thousand files, a shell test or two, and for most of them an ls and an awk. Somewhere around a million processes to answer questions that find has been able to answer itself since the late nineties.

So the scan is now one find. It writes each class of file to its own list as it goes, in one pass over the disk, and finds dangling symlinks with -xtype l instead of scraping an error message. The same scan on the same machine now takes a minute and fifty seconds and a tenth of the system time, and the dangling-symlink warnings have paths in them. On a system whose find cannot do this (busybox, mostly) the old loop is still there as a fallback.

I care about this more than the speed would suggest. Tigris has to run on a Raspberry Pi in a cupboard and on a small VPS, not only on a workstation with sixteen cores, so every change this evening was measured for wall time, CPU and peak memory, and that will stay true. The full run is at 130 MB peak at the moment, which is more than it should be and is on the list.

What it found, once it ran

By the end of the evening the report was 318 lines. Some of the reduction was fixing the bugs above. Some was teaching old checks about a world they had not met: Flatpak's OSTree store and Docker's image layers are full of symlinks that only resolve inside their own root, and no host audit should list eleven thousand of them; /dev/ptmx and /dev/fuse are world-writable on purpose; systemd-resolve has / as its home directory and cannot log in, so it does not need one; Debian has called its log /var/log/syslog rather than /var/log/messages for twenty years. None of that is clever. It is the perished rubber again.

What was left was real, and some of it I did not know about my own machine:

  • It accepts source-routed packets and ICMP redirects, and does not log martians. All three are one sysctl each.
  • IP forwarding is on. Docker did that, and it is right, but it is good to be told.
  • sshd still allows password authentication.
  • Two xrdp units are enabled in multi-user.target.wants whose service files no longer exist. Half an uninstall, sitting there since who knows when.
  • Eleven files differ from what their packages installed. The list matches dpkg --verify exactly, which is how I know the check is now telling the truth.
  • /dev/kmsg is world-readable, so any user can read the kernel log.

That is a report I would read. It is also, apart from the kernel log, more or less what Lynis would have told me, so the fair question is why not just run Lynis. The answer is the two things from earlier: the package-integrity check that found those eleven files, and the change-only mode, both of which Lynis lacks and both of which TIGER had in 1993 and still has.

What it is now

I have called the fork Tigris, which is the Latin for tiger and the root of the Icelandic tígrisdýr; there was never a Norse word for an animal no Norseman saw, so they borrowed the Roman one, and so have I. It lives at github.com/creativeheadz/tigris, under the same GPL it has always had, with the full history back to 1993 intact. It is a fork. The official TIGER remains on Savannah, and the fixes from this weekend that apply to the Debian package have gone to the Debian bug tracker as patches, where they are of use to the twenty years of users who have it installed already.

It now has what a 2026 project needs and a 1993 one never had: tests that plant a modified binary, a deleted one and a stray file in a clean Debian container and check that exactly those three are reported, and a CI pipeline that runs them on Debian stable, Debian unstable and Ubuntu, plus a full audit on a fresh Ubuntu runner, on every push. The first run of that pipeline found a bug in the build. That is what it is for.

Where it goes

The roadmap is in the repository, and the short version is this.

First, the modern Linux baseline: retire the checks for things that no longer exist (inetd, rhosts, anonymous FTP, LILO) and add the ones for things that do (systemd units and their hardening, nftables, sshd -T, sudoers.d, kernel mitigations, LUKS, automatic updates). Package integrity for rpm, apk and pacman alongside dpkg, because the dpkg one is the best thing in the tool and it should not be Debian-only. Half the source tree is configuration for AIX, IRIX, NeXT and UNICOS; that goes into an attic tag with respect, and stays in the history.

Then the things that would make it better than the alternatives rather than merely as good. A JSON report with a schema, so a platform like Wegweiser can read it without parsing prose. One metadata file per check, with its severity, its fix and the compliance controls it maps to, so the documentation, the explanations and the report are generated from the same source and cannot drift. A free mapping to CIS, ISO 27001 and Cyber Essentials, which the commercial tools charge for. Drift as a first-class feature: tigris diff between two runs, and a way to accept a finding with a reason and an expiry date, which is the grown-up version of what tigercron was already doing from a cron job in 1994. And auditing a mounted disk image or a container's root filesystem without booting it, which nothing in this space does well.

After that, perhaps, Windows. Not by running shell on it, but by writing a second engine in PowerShell against the same finding IDs, the same JSON and the same compliance mapping, once those are fixed. Windows has nothing like this in the open, and it has everything an auditor needs exposed through WMI, the registry and the event log. Mayhap.

The receipts

I want to end where I started, with the glovebox. The reason this was worth a Saturday is not that the code was good, although much of it is. It is that it was documented. Every check has a header listing what changed and when, back to 1993, with the Debian bug number beside each fix. Every message code has an explanation you can look up. The maintainer's changelog runs to a thousand entries. When I found the ownership refusal, the kernel-version fallback and the error-message scrape, I could see in each case what the author had been thinking and why it had been right at the time.

That is rarer than good code, and it is the thing that makes old code worth waking up. You are not guessing. You are reading the receipts, and then you are changing the hoses.