Explain cron and systemd timer evidence without running scheduled work.
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-18 01:52:46 +00:00
.forgejo/workflows Use Codeberg hosted Actions runner 2026-07-18 01:52:46 +00:00
bin Build TimerProof MVP 2026-07-18 01:18:36 +00:00
docs Build TimerProof MVP 2026-07-18 01:18:36 +00:00
examples Build TimerProof MVP 2026-07-18 01:18:36 +00:00
scripts Build TimerProof MVP 2026-07-18 01:18:36 +00:00
src Build TimerProof MVP 2026-07-18 01:18:36 +00:00
test Build TimerProof MVP 2026-07-18 01:18:36 +00:00
.gitignore Build TimerProof MVP 2026-07-18 01:18:36 +00:00
CHANGELOG.md Build TimerProof MVP 2026-07-18 01:18:36 +00:00
LICENSE Build TimerProof MVP 2026-07-18 01:18:36 +00:00
package-lock.json Build TimerProof MVP 2026-07-18 01:18:36 +00:00
package.json Build TimerProof MVP 2026-07-18 01:18:36 +00:00
PRIVACY.md Build TimerProof MVP 2026-07-18 01:18:36 +00:00
README.md Build TimerProof MVP 2026-07-18 01:18:36 +00:00
SECURITY.md Build TimerProof MVP 2026-07-18 01:18:36 +00:00

TimerProof

TimerProof is a local-only, read-only Node.js CLI and API for inventorying classic cron entries and systemd timers on Linux. It separates configured schedule evidence, manager snapshots, and unknowns so a timer listing is not mistaken for proof that a command or service succeeded.

TimerProof does not run jobs, mutate scheduler state, inspect logs, contact network services, or collect telemetry. Cron command text is omitted unless --include-commands or includeCommands: true is explicitly supplied.

Requirements

  • Node.js 20 or newer
  • Linux for live cron and systemd inventory
  • crontab and/or systemctl when those scheduler views are wanted

The pure parsers and formatters can be used on any platform supported by Node.js.

Run from a checkout

npm ci --ignore-scripts
node bin/timerproof.js --help
node bin/timerproof.js
node bin/timerproof.js --json --scope system

To install a locally built tarball:

npm pack
npm install --global ./timerproof-0.1.0.tgz

The package has no runtime dependencies.

CLI

timerproof [options]

Common options:

  • --json emits canonical, recursively key-sorted JSON.
  • --scope all|system|user selects scheduler views.
  • --observed-at <date> sets the report's comparison reference. Cron is evaluated at that host-local minute; live systemd rows remain snapshots collected during the run.
  • --include-commands reveals cron command text. Commands remain omitted by default.
  • --timeout-ms, --max-bytes, --max-units, and --max-cron-sources reduce or raise bounded inspection limits.

A completed report exits with status 0 even when a scheduler is absent, inaccessible, or only partially readable; those conditions are represented in the report. Invalid CLI input or an inability to create a report exits with status 2.

See docs/cli.md for the full behavior and docs/evidence-model.md for interpretation.

API

import { formatHuman, inventory, stableStringify } from "timerproof";

const report = await inventory({
  scope: "system",
  observedAt: "2026-07-18T12:00:00.000Z",
});

process.stdout.write(formatHuman(report));
process.stdout.write(`${stableStringify(report)}\n`);

The package also exports bounded cron parsers, structured systemd list parsers, a parser for already-captured systemctl show text, and the default fixed-allowlist command runner. See examples/api.mjs.

What is inspected

Default live inventory is deliberately narrow:

  • /etc/crontab
  • portable regular files in /etc/cron.d
  • crontab -l
  • fixed systemctl --system and systemctl --user list-timers invocations using JSON output
  • fixed systemctl --system and systemctl --user list-unit-files invocations using JSON output

No discovered value is ever inserted into a subprocess argument. In particular, live inventory does not invoke systemctl show for discovered units. The exported parseSystemdShow function only parses text supplied by the caller.

Supported cron grammar

TimerProof evaluates numeric five-field expressions with:

  • *
  • comma lists
  • inclusive numeric ranges
  • steps on * or ranges
  • weekday 0 or 7 for Sunday

Cron's day-of-month/day-of-week OR rule is applied. A field containing a wildcard list item, including 1,*, is treated as wildcard-bearing for that rule. Names, nicknames such as @reboot, implementation-specific extensions, and timezone assignments are reported as unsupported or unknown instead of guessed.

Files whose names are ignored by cron's portable /etc/cron.d naming rule, including .placeholder, are reported as ignored and do not degrade manager status.

Evidence boundaries

  • Cron eligibility means only that a supported expression matches the observed host-local minute.
  • A systemd next-elapse value is a manager snapshot, not a guarantee of future activation.
  • A systemd last-trigger timestamp is evidence that the timer manager recorded a trigger, not proof that the activated service started or succeeded.
  • Unit-file state is installation or enablement evidence, not runtime state.
  • TimerProof does not parse unit files, inspect journal history, or establish command outcomes.

Safety and privacy

Read SECURITY.md and PRIVACY.md. The short version is that the default implementation has a fixed read-only subprocess allowlist, uses no shell, applies output and time limits, sanitizes terminal control sequences, performs no network I/O, and stores nothing.

Development

npm run check
npm test
npm run test:coverage
npm run test:all
npm pack --dry-run
npm run test:tarball

npm test refuses to pass if no *.test.js files exist. npm run check recursively enumerates every .js, .mjs, and .cjs file under the package source, scripts, tests, examples, and binary directories before running node --check on each one.

License

MIT