-
Notifications
You must be signed in to change notification settings - Fork 1
Configuration and 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 | nonefettle --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.
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 = falseunder[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].
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 firstIt reads whatever JSON is currently retained (the
keepwindow), so run it after your scans; for older text-only reports, run--backfill-jsononce.
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 livefettle 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 runssudo fettle <action> --yeswith 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 webbinds127.0.0.1and rejects any non-localhostHost(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 thesudosubprocess it spawns is elevated.