Skip to content

Configuration and reporting

Paul Asadoorian edited this page Aug 17, 2026 · 2 revisions

Configuration & reporting

Optional TOML file at ~/.config/fettle/config.toml. Precedence, low → high: built-in defaults < config file < command-line flags. fettle refuses to read a config that is world-writable or owned by someone other than you or root — it does not reject a world-readable one, so chmod 600 it yourself if it holds a secret. Action names accept hyphens or underscores.

# ~/.config/fettle/config.toml  (all keys optional; values shown are the defaults)

default_actions = ["clean", "orphans", "update", "rebuild-check", "python-rebuild-check", "config-drift", "auto-updates", "firmware-check", "pkg-audit"]
auto_rebuild    = false
exclude_foreign = ["brave-bin", "google-chrome"]   # names or globs; skip in reports
keep_orphans    = ["downgrade", "nvchecker"]        # never offer these for removal

# AUR supply-chain
aur_max_age_days  = 365    # PKGBUILD older than this is "stale" (pkg-audit)
aur_recent_days   = 21     # -A flags packages changed within this window
aur_ioc_campaigns = ["aur-infected", "chaos-rat", "russian-spam"]
aur_ioc_cache_ttl = 21600  # seconds to cache IOC feeds on disk
aur_precheck_on_update = true  # IoC-check AUR pkgs before yay builds them (--no-aur-precheck skips)

# Upgrade Checker (fettle upgrade-check) [experimental] — prefer ANTHROPIC_API_KEY env var
ai_model            = "claude-sonnet-5"
ai_effort           = "medium"   # low | medium | high — thinking depth vs cost
ai_max_web_searches = 5          # cap forum searches per run (bounds tokens/cost)
# ai_api_key = "sk-ant-..."      # optional; keep the file chmod 600; never printed in full

# Cache cleaning (fettle -c)
[clean]
keep_versions = 2        # Arch/Manjaro: cached versions to keep per INSTALLED package.
                         # Cached packages you no longer have installed are always
                         # removed — they have no rollback value. 0 keeps none, which
                         # frees the most and leaves no offline downgrade path.
                         # Needs pacman-contrib (paccache); without it fettle falls
                         # back to `pacman -Sc`, which keeps only the installed version.

# Reports & run logs (stored under ~/.fettle/, per host, 0600)
[reports]
keep = 5                 # how many of each report/log to keep per host
# dir = "~/.fettle"      # base dir override (reports/ and logs/ live under it)
# log = true             # record a per-run transcript (set false to disable)

# Per-distro tool selection
[updaters.arch]
refresh_mirrors = true   # regenerate /etc/pacman.d/mirrorlist before upgrading (Manjaro).
                         # ON by default: a mirror that has fallen behind serves an old
                         # database, and the upgrade then resolves against packages it no
                         # longer has. false = never touch the mirrorlist. An integer N
                         # = the fastest N mirrors (`pacman-mirrors -f N`) — worth setting,
                         # since bare -f speed-tests EVERY known mirror on every upgrade.
system_updater = "pacman"   # pacman | pamac
aur_updater    = "yay"      # yay | pamac | none

[updaters.debian]
system_updater  = "apt"      # apt | nala | none
flatpak_updater = "flatpak"  # flatpak | none
snap_updater    = "snap"     # snap | none

fettle --print-config shows the effective configuration; --config PATH points at an alternate file; --no-config ignores it entirely. A starter template ships as fettle.toml.example.

Reports & run logs

Every report (aur-audit, pkg-audit, hardening-audit, upgrade-check, the orphans list, …) is written under ~/.fettle/reports/<host>/, timestamped so runs never clobber each other, chmod 0600 (they name your packages and can hold system detail), and rotated to the newest keep (default 5) per host, per report type. <host> is local for a local run or the target hostname for fettle remote <host> …, so each machine keeps its own history. (Pre-0.11 reports in $HOME are left untouched; fettle notes the move once.)

Every invocation is also recorded to a transcript under ~/.fettle/logs/<host>/run-<timestamp>.txt (same 0600 + rotation). On an interactive terminal fettle captures the whole session — its own output and every tool it runs (yay/pacman/apt) — the way script(1) does: it re-execs itself once under a pseudo-terminal, so the actual run happens on a real tty and colours, progress bars, and sudo/PKGBUILD prompts behave exactly as normal. The saved log is ANSI-stripped for readability. When output is piped or non-interactive there's no terminal to record, so the log captures fettle's own output only.

The one-time re-exec is transparent, but if you're debugging startup or wrapping fettle in another tool and want it off, set log = false under [reports].

JSON siblings. Every report and log is also written as a structured <name>-<timestamp>.json beside the .txt — a {schema, tool, host, timestamp, fettle_version, data} envelope whose data is the real structure (scored hardening packages, findings with severity, the upgrade-check result, package lists, log transcript). Same 0600, rotated as a unit with the .txt. Turn it off with json = false under [reports].

HTML report — fettle report

Each host card is a verdict across every audit, not a hardening tally: the worst severity found in the newest report of each type, then the two or three findings that drove it. A host that has stopped reporting is itself a finding ([reports] stale_days, default 7) — at fleet scale, no data is not good news.

fettle-arch    [High]    · 2 firmware/boot finding(s) needing attention
                         · 34 packages missing build hardening
ec3            [Medium]  · no audit in 13 days
bifrost-lab    [Medium]  · no audit has run on this host — only run logs,
                           which check nothing

"Newest report of each type" means newest, including a clean one. Until v1.12.0 clean reports were discarded before the newest was picked, so "newest" silently meant the most recent report that found something — and a finding you had fixed stayed on the card forever. Measured on a real fleet: a host showed 47 CVEs (17 High) that had been cleared thirteen days earlier. If the newest run of a tool found nothing, that is what the card says.

A run log is not an audit. fettle -u writes a transcript and checks nothing, so a host that only ever updates has recent activity and no security coverage at all. Freshness is measured from reports only — the staleness chip says "no audit in N days" for that reason, since the card's latest: line still counts run-log activity. A host with no reports whatsoever says so rather than showing a green OK. A clean audit still counts as coverage: an empty report is an answer, an absent one is not.

What changed since you last looked. Each card carries +11 new, -47 resolved since 2026-07-24, and the newest report of each type gets a +4 / -2 badge whose tooltip names the packages. The baseline is the newest report from an earlier calendar day, not the previous report — three runs in an hour would otherwise reset it and show an empty delta right after you fixed something. sys-audit and pkg-integrity record no per-finding identity, so those honestly report only a count change.

Resolved findings are shown as prominently as new ones: "you fixed it" must not render the same as "it was never there".

A severity filter sits beside the host/type/grep filters — Critical, High and above, and so on — hiding host cards below the threshold along with report entries whose worst finding is below it. Entries carrying no findings at all (run-logs, package lists) are hidden by it too: asking for "High and above" and getting a run-log back is not an answer to the question.

[reports] keep defaults to 10 (raised from 5 in v0.81.0) because retention is also the depth of this history — five rotated out of a single busy afternoon.

hardening-audit is deliberately capped at Medium on the card, for the same reason -H does not fail the run: its "Critical" is the worst band of a scoring scheme that every real desktop lands in, and letting it dominate the fleet view teaches you to ignore the colour.

Everything that names something is a link. Package names go to wherever that package actually lives, and every advisory identifier to the authority that holds it:

links to
arch/openssl (repo package) archlinux.org/packages/?name=openssl
AUR / foreign packages aur.archlinux.org/packages/<name>
apt · dnf · flatpak · snap that ecosystem's package page
osv/certifi (language package) pypi.org / npmjs.com / crates.io, by recorded ecosystem
CVE-… nvd.nist.gov
GHSA-… github.com/advisories — many never reach NVD
UBUNTU-CVE-… ubuntu.com/security — it carries the per-release fix status
AVG- · DSA- · USN- that distro's own tracker

Note the first two rows are different places on purpose: advisory-check's arch findings come from security.archlinux.org, which tracks core/extra — the AUR packages are the ones in its "not covered by the tracker" list. An identifier fettle does not recognise stays plain text rather than being guessed at.

One severity scale (Critical / High / Medium / Low / Info) across supply-chain and advisory findings, in the terminal and in the JSON. They used to differ — LOW: 38 and Low: 510 could appear in the same view meaning different things — which made sorting or filtering across them impossible. Reports written before v0.80.0 are normalised on read.

fettle report regenerates a single self-contained ~/.fettle/report.html (0600) from all the stored JSON, across every host: a per-host summary card row (latest hardening band tally, per-type counts, latest run), collapsible sections grouped by report type with native rendering — scored hardening tables, severity-coloured findings, upgrade verdicts, package lists, sys-audit firmware/boot/hardware results (status levels + a raw-output section), log transcripts — and a host/type/text filter. Each entry shows the exact command that produced it (a $ fettle … chip). AUR package names link to their AUR page (in the health, supply-chain, and IOC reports), and the AUR Package Health table shows a software column — the package description plus a link to the upstream project. Empty reports are hidden (with a per-host "N hidden" note). It's styled as a dark Linux terminal (monospace, phosphor palette). Pure stdlib, no external assets, nothing served.

fettle report                 # (re)build ~/.fettle/report.html
fettle report --open          # …and open it in a browser
fettle report --backfill-json # one-off: give pre-0.12 .txt reports a JSON sibling first

It reads whatever JSON is currently retained (the keep window), so run it after your scans; for older text-only reports, run --backfill-json once.

Web UI — fettle web (experimental, optional)

Experimental, and the one part of fettle the QA pass has not reached. Every other feature has been swept feature-by-feature against real hosts and the VM lab (docs/qa/); the web UI has not, and it is the surface that both serves a page and runs privileged actions with a password you type into a browser. Treat its output as unverified and its action buttons as unaudited until that sweep happens. The CLI it drives is the tested part.

A browser interface over the same data and actions, built on NiceGUI. It's an opt-in extra so the CLI core stays pure-stdlib (the stdlib-only remote zipapp is unaffected):

pip install 'fettle[web]'
fettle web                    # serves http://127.0.0.1:8080
fettle web --port 9000        # a different port
  • Dashboard (/) — the live fettle report, generated on each load, for every host, with a refresh button. (Reuses the report renderers verbatim.)
  • Run (/run) — a button per read-only audit (runs unprivileged, output streams live), and a system-maintenance section for the privileged actions (update, clean, orphans, kernel, …): each has a Preview (a --dry-run) and a Run (sudo) that confirms first, then runs sudo fettle <action> --yes with a sudo password you type on the page (kept in memory, never stored/logged).
  • Remote (/remote) — run on a configured [remote.groups.<name>] or an ad-hoc host over SSH; each host's results come back to the dashboard.
  • History (/history) — every stored run across all hosts, newest first (when · host · fettle <argv> · ok/exit), each expandable to its transcript.

Localhost-only, single-operator. fettle web binds 127.0.0.1 and rejects any non-localhost Host (DNS-rebinding defense). Web-triggered actions are logged to ~/.fettle/web-actions.log (0600). It has no authentication — do not expose it to a network; put it behind your own auth/VPN if you must. The web server runs unprivileged; only the sudo subprocess it spawns is elevated.

Clone this wiki locally