Add persistence scanner, scan history, notifications, and native-panel UI - #5
Conversation
Fixes elynch303#1 (partially -- AUR-Malware's dead upstream URL is a separate, unresolved problem left out of this PR on purpose). - SecurityWidget.qml: bbInstallProc targeted the bumblebee module root (github.com/perplexityai/bumblebee@latest), which go install rejects ('found, but does not contain package ...'). The installable binary lives at cmd/bumblebee. Verified working end-to-end after the fix. - qs-security-scan.sh + systemd/qs-security-scan.{service,timer}: the README documents this script and timer as what feeds the widget's status file, but neither was actually shipped in the repo -- so even a correctly installed scanner never produced a result. Adds the script (skips AUR-Malware cleanly when it's not installed, runs bumblebee scan and summarizes package/finding counts) and the unit files, plus wires an opt-in step into install.sh mirroring the existing bun-check flow. Tested on a real machine: bumblebee installs and scans correctly, qs-security-scan.timer runs on the 6h schedule and writes a status file the widget reads and displays.
Atomic-Arch/AUR-Malware (what the install button cloned and the README pointed at) has been deleted -- clone always failed, surfaced as the generic "maybe git isn't in PATH" error. The fallback destination, /local/applications/AUR-Malware, also isn't writable by a normal user (no sudo in this flow), so even a working URL would have failed there. nightdevil00/AUR-Malware -- already linked in this repo's own GitHub "About" section as the intended source, just never wired into the README or the install button -- is a maintained fork shipping the same check-atomic-arch_new.sh entry point. Repointed the install button, default path/env-var fallback, and README to it, using ~/.local/share/AUR-Malware instead of the root-owned path. Also fixes the AUR-Malware branch of qs-security-scan.sh, which never surfaced findings: the script's exit code is always 0 regardless of result, and the previous "last output line" heuristic picked up a disclaimer fragment instead of a verdict. Switched to --json and parse the trailing JSON object (verdict + pass/warn/fail counts) directly. Fixes elynch303#1.
The main popup only ever had room for a status word and a one-line
summary ("2 failures, 1 warning out of 18 checks") -- it never said
WHICH checks failed or why, so acting on a finding meant running the
scanner by hand in a terminal.
Detail view (accessed via a "View detail" button, same pattern as the
existing Settings view):
- Findings are grouped by source when one can be identified: a known-
infected package name (straight from the scanner's own output), an
npm/bun malicious-indicator package name, the package owning a
flagged file path (pacman -Qo), or the comment sitting above a
flagged /etc/hosts entry. Ungrouped findings fall back to "OTHER",
sorted after the named groups.
- Each finding shows its check name and full detail text, with a
fail/warn icon and color.
Dismiss/reactivate a reviewed false positive:
- Only for checks that qs-security-scan.sh marks dismissible --
anything that is direct evidence of an actual compromise (known-
infected package, malicious npm/bun indicator, eBPF artifacts,
hidden processes, pacman log hits, ld.so.preload injection,
persistence artifacts) has no dismiss button at all. This is what
keeps dismiss safe on a malware-detection tool: you can silence
heuristic noise (a stale /etc/hosts entry, a shell config edited
after the incident date), never the finding that actually matters.
- Identified by check name + a hash of the exact detail text, so if
the detail changes (a new infected package appears, a date moves)
the old dismissal stops matching and the finding is active again on
its own.
- The badge color and summary count only active (non-dismissed)
findings, otherwise dismissing a known false positive would not
stop the badge going red for the same thing every 6h.
- qs-security-dismiss.sh recomputes the existing status file directly
instead of rerunning the scanner (~13s for the network fetch + all
18 checks) -- dismissing is bookkeeping over an already-run scan, it
doesn't need a fresh one to show up in the badge.
…l UI Builds on top of elynch303#3 (reuses IssueDetailView / groupIssues / dismiss infrastructure for a second scanner). ## New scanner: persistence Bash-only, no install step, always active. Scans autostart .desktop entries, user systemd units (real files, not the *.wants/ enablement symlinks) and crontab for the same injection-pattern heuristic AUR-Malware already uses for shell configs (curl|bash, eval "$(curl...)", base64 -d |, etc). Same detail view, grouping, and dismiss/reactivate flow as AUR-Malware, sharing the IssueRow/IssueDetailView components and qs-security-dismiss.sh (now takes an optional 4th "block key" argument so it can recompute either aur_malware or persistence). ## Scan history Each real scan (not ones skipped for gaming) appends a compact entry to ~/.local/share/qs-security/history.json (capped at 30). A new "History" view, opened from a clock icon in the header, lists past results newest-first so a finding that's since cleared still left a trace instead of vanishing the moment the next scan overwrites the status file. ## Gaming-aware timer + "next scan" display qs-security-scan.sh now checks `gamemoded -s` itself and exits early while a game is active, logging that (and the real timestamp on a normal run) to ~/.cache/qs-security-last-run.json. The popup header shows "next scan in Xh" normally, or "skipped Xm ago (game active) · next in Xh" when a run was preempted -- previously a paused scan just looked like the widget had silently stopped working. This is independent of and complementary to elynch303#4's systemd ExecCondition (which stops the unit from ever starting); doing the check in-script as well means there's something to log even if that unit change isn't present. ## Desktop notifications A real notify-send for genuinely NEW active findings (tracked by fingerprint in ~/.config/qs-security/notified.json, so already-seen or already-dismissed findings stay quiet) -- the badge changing color is invisible to anyone not already looking at the bar. bumblebee has no per-finding fingerprint, so it only notifies on the clean -> findings transition by comparing against the previous history entry. ## UI: match Omarchy's native panel design language Compared this widget against Omarchy's 10 first-party bar panels (Audio, Network, Bluetooth, Power, Dropbox, Tailscale...) and found a few real gaps: - Root type BarWidget + PopupCard -> Panel + KeyboardPanel, same as every native panel -- gains keyboard navigation (Escape closes, Tab switches between bar panels) the widget didn't have before. - Header rebuilt with the shared PanelHero component (icon + title + uppercase status line), matching Dropbox/Tailscale's pattern, instead of a hand-rolled Text+PanelActionButton row. - Colors now read from the active bar (root.foreground/root.urgent, same fallback pattern Dropbox/Tailscale use) instead of generic Color.popups.text/Color.urgent tokens, so the widget actually follows the active theme like the rest of the bar. - contentWidth 300 -> 380 (every native panel but Clock/Weather uses 380), and section spacing brought in line with Network's actual values -- separators are now a sibling of their content column (with the view's bigger inter-section spacing) rather than nested inside it sharing the same tight spacing, matching how Network's own "DNS provider" section is built. ## Testing Verified end-to-end on a real machine: persistence scanner catches a synthetic curl|bash systemd unit and clears once removed; dismiss/ reactivate works for both aur_malware and persistence blocks; history accumulates across real scans; notification fires once for a new finding and stays quiet on rescan; gamemode skip logged correctly with gamemoded running. qmllint clean, no brace/id mismatches. Happy to split this differently (e.g. persistence scanner separate from the UI refresh) if that's easier to review -- just let me know.
…are fixes, detail view + dismiss, persistence scanner + history + notifications + native-panel UI
badgeColor() already switched to root.urgent/root.foreground for the alert states, but the "all clean" case still fell through to Color.accent -- the same generic token this PR moved everything else away from. Since the badge's `active` flag is true whenever any scanner is installed (not just when there's something to report), this made the badge sit permanently in the theme's accent color instead of blending in with the rest of the bar like Bluetooth/Network/Volume do at rest. Only the clean-state return changes; fail/warn keep standing out on purpose. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed one more small fix on top: |
The previous commit (c0ce361) moved the clean-state badge color from Color.accent to root.foreground, but root.foreground is bar.foreground -- the theme's static token. WidgetButton's own default fallback (used by every other bar icon when it's not signaling something) is bar.barForeground instead: a dynamic value Bar.qml recomputes against whatever is actually behind a transparent/blurred bar (transparentForegroundProc), so icons stay legible regardless of theme or wallpaper. This widget's `active` is true almost anytime a scanner is installed (not just when there's something to report), so it always goes through activeColor/badgeColor() and never falls through to that dynamic default like other icons do. Net effect: the badge could render lighter or darker than the rest of the bar depending on what the bar's contrast logic decided at that moment, confirmed with before/after screenshots against Bluetooth/Network/Volume on the same bar. Only the clean-state return changes; fail/warn/setup states are unaffected on purpose -- they're meant to stand out regardless of the bar's backdrop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed one more commit here: the clean-state badge color still didn't quite match the rest of the bar in some cases. Root cause was that Confirmed with before/after screenshots against Bluetooth/Network/Volume on the same bar. Only the clean-state return changes. |
Depends on #3 (reuses
IssueDetailView/groupIssues/ dismiss infrastructure for a second scanner).New scanner: persistence
Bash-only, no install step, always active. Scans autostart
.desktopentries, user systemd units (real files, not the*.wants/enablement symlinks) and crontab for the same injection-pattern heuristic AUR-Malware already uses for shell configs (curl|bash,eval "$(curl...)",base64 -d |, etc). Same detail view, grouping, and dismiss/reactivate flow as AUR-Malware, sharing theIssueRow/IssueDetailViewcomponents andqs-security-dismiss.sh(now takes an optional 4th "block key" argument so it can recompute eitheraur_malwareorpersistence).Scan history
Each real scan (not ones skipped for gaming) appends a compact entry to
~/.local/share/qs-security/history.json(capped at 30). A new "History" view, opened from a clock icon in the header, lists past results newest-first, so a finding that's since cleared still left a trace instead of vanishing the moment the next scan overwrites the status file.Gaming-aware timer + "next scan" display
qs-security-scan.shnow checksgamemoded -sitself and exits early while a game is active, logging that (and the real timestamp on a normal run) to~/.cache/qs-security-last-run.json. The popup header shows "next scan in Xh" normally, or "skipped Xm ago (game active) · next in Xh" when a run was preempted — previously a paused scan just looked like the widget had silently stopped working. This is independent of and complementary to #4's systemdExecCondition(which stops the unit from ever starting); doing the check in-script as well means there's something to log even if that unit change isn't present.Desktop notifications
A real
notify-sendfor genuinely NEW active findings (tracked by fingerprint in~/.config/qs-security/notified.json, so already-seen or already-dismissed findings stay quiet) — the badge changing color is invisible to anyone not already looking at the bar. bumblebee has no per-finding fingerprint, so it only notifies on the clean → findings transition by comparing against the previous history entry.UI: match Omarchy's native panel design language
Compared this widget against Omarchy's 10 first-party bar panels (Audio, Network, Bluetooth, Power, Dropbox, Tailscale...) and found a few real gaps:
BarWidget+PopupCard→Panel+KeyboardPanel, same as every native panel — gains keyboard navigation (Escape closes, Tab switches between bar panels) the widget didn't have before.PanelHerocomponent (icon + title + uppercase status line), matching Dropbox/Tailscale's pattern, instead of a hand-rolledText+PanelActionButtonrow.root.foreground/root.urgent, same fallback pattern Dropbox/Tailscale use) instead of genericColor.popups.text/Color.urgenttokens, so the widget actually follows the active theme like the rest of the bar.contentWidth300 → 380 (every native panel but Clock/Weather uses 380), and section spacing brought in line with Network's actual values — separators are now a sibling of their content column (with the view's bigger inter-section spacing) rather than nested inside it sharing the same tight spacing, matching how Network's own "DNS provider" section is built.Testing
Verified end-to-end on a real machine: persistence scanner catches a synthetic
curl|bashsystemd unit and clears once removed; dismiss/reactivate works for bothaur_malwareandpersistenceblocks; history accumulates across real scans; notification fires once for a new finding and stays quiet on rescan; gamemode skip logged correctly withgamemodedrunning.qmllintclean, no brace/id mismatches.Happy to split this differently (e.g. persistence scanner separate from the UI refresh) if that's easier to review — just let me know, and no worries at all if you'd rather take any of this in a different direction!