diff --git a/README.md b/README.md index 24ad9f6..ff0c39d 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ An [Omarchy](https://omarchy.com) bar-widget plugin that shows a live security b | Scanner | What it checks | How to install | |---|---|---| -| **AUR-Malware** | Atomic Arch IOC scan — pacman/AUR packages, npm/bun caches, eBPF rootkit artifacts, hidden processes | Clone [AUR-Malware](https://github.com/Atomic-Arch/AUR-Malware) to `/local/applications/AUR-Malware/` | +| **AUR-Malware** | Atomic Arch IOC scan — pacman/AUR packages, npm/bun caches, eBPF rootkit artifacts, hidden processes | Clone [AUR-Malware](https://github.com/nightdevil00/AUR-Malware) to `~/.local/share/AUR-Malware/` (the original `Atomic-Arch/AUR-Malware` this pointed at is gone; this fork ships the same `check-atomic-arch_new.sh`) | | **[bumblebee](https://github.com/perplexityai/bumblebee)** | Endpoint package inventory across npm, pypi, go, rubygems, homebrew, etc. | `GOBIN=$HOME/.local/bin go install github.com/perplexityai/bumblebee@latest` | | **bun-check** | Per-project dev-env one-shot scan (opens a terminal picker) | Bundled — run `install.sh` after adding the plugin | diff --git a/SecurityWidget.qml b/SecurityWidget.qml index 7ee31c6..a009feb 100644 --- a/SecurityWidget.qml +++ b/SecurityWidget.qml @@ -15,7 +15,7 @@ import qs.Ui // — preferences persisted to ~/.config/qs-security/settings.json // // Scanners: -// AUR-Malware git clone to /local/applications/AUR-Malware (or QS_SEC_AUR_MALWARE) +// AUR-Malware git clone to ~/.local/share/AUR-Malware (or QS_SEC_AUR_MALWARE) // bumblebee go install github.com/anchore/bumblebee@latest // bun-check bundled script → ~/.local/bin/qs-bun-check-oneshot.sh BarWidget { @@ -67,12 +67,12 @@ BarWidget { readonly property string aurEffectivePath: { var ov = Quickshell.env("QS_SEC_AUR_MALWARE") - return ov ? ov : "/local/applications/AUR-Malware/check-atomic-arch_new.sh" + return ov ? ov : root.home + "/.local/share/AUR-Malware/check-atomic-arch_new.sh" } readonly property string aurMalwareDir: { var p = root.aurEffectivePath var i = p.lastIndexOf("/") - return i > 0 ? p.substring(0, i) : "/local/applications/AUR-Malware" + return i > 0 ? p.substring(0, i) : root.home + "/.local/share/AUR-Malware" } readonly property string bunDst: home + "/.local/bin/qs-bun-check-oneshot.sh" @@ -249,7 +249,7 @@ BarWidget { root.aurOpBusy = true; root.aurOpMsg = ""; root.aurOpError = false aurInstallProc.command = [ "bash", "-c", - "mkdir -p \"$(dirname \"$0\")\" && git clone https://github.com/Atomic-Arch/AUR-Malware.git \"$0\"", + "mkdir -p \"$(dirname \"$0\")\" && git clone https://github.com/nightdevil00/AUR-Malware.git \"$0\"", root.aurMalwareDir ] aurInstallProc.running = false; aurInstallProc.running = true @@ -264,7 +264,7 @@ BarWidget { root.bbOpBusy = true; root.bbOpMsg = "Installing via go…"; root.bbOpError = false bbInstallProc.command = [ "/usr/bin/mise", "exec", "--", "sh", "-c", - "GOBIN=$HOME/.local/bin go install github.com/perplexityai/bumblebee@latest" + "GOBIN=$HOME/.local/bin go install github.com/perplexityai/bumblebee/cmd/bumblebee@latest" ] bbInstallProc.running = false; bbInstallProc.running = true } diff --git a/install.sh b/install.sh index a0f0373..0719422 100755 --- a/install.sh +++ b/install.sh @@ -1,25 +1,41 @@ #!/usr/bin/env bash # Optional install step for the Security Scan plugin. -# Copies qs-bun-check-oneshot.sh to ~/.local/bin/ so the widget can -# show the per-project bun-check scan button. +# - Copies qs-bun-check-oneshot.sh to ~/.local/bin/ so the widget can +# show the per-project bun-check scan button. +# - Copies qs-security-scan.sh to ~/.local/bin/ and enables the systemd +# user timer that runs it every 6h -- without this, the widget has +# nothing writing ~/.cache/qs-security-status.json and never shows a +# result even once a scanner is installed. # # Usage: -# bash install.sh # asks whether to install bun-check -# bash install.sh --bun-check # install bun-check without prompting -# bash install.sh --no-bun-check # skip bun-check +# bash install.sh # asks about both steps +# bash install.sh --bun-check # install bun-check without prompting +# bash install.sh --no-bun-check # skip bun-check +# bash install.sh --scan-timer # install+enable the scan timer without prompting +# bash install.sh --no-scan-timer # skip the scan timer set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BIN_DIR="${HOME}/.local/bin" +SYSTEMD_DIR="${HOME}/.config/systemd/user" + BUN_CHECK_SRC="$SCRIPT_DIR/qs-bun-check-oneshot.sh" BUN_CHECK_DST="$BIN_DIR/qs-bun-check-oneshot.sh" +SCAN_SCRIPT_SRC="$SCRIPT_DIR/qs-security-scan.sh" +SCAN_SCRIPT_DST="$BIN_DIR/qs-security-scan.sh" +SCAN_SERVICE_SRC="$SCRIPT_DIR/systemd/qs-security-scan.service" +SCAN_TIMER_SRC="$SCRIPT_DIR/systemd/qs-security-scan.timer" + install_bun_check= +install_scan_timer= for arg in "$@"; do case "$arg" in --bun-check) install_bun_check=true ;; --no-bun-check) install_bun_check=false ;; + --scan-timer) install_scan_timer=true ;; + --no-scan-timer) install_scan_timer=false ;; esac done @@ -36,3 +52,21 @@ if [[ "$install_bun_check" == true ]]; then else echo "Skipped bun-check install. Run with --bun-check later to add it." fi + +if [[ -z "$install_scan_timer" ]]; then + read -rp "Install and enable the periodic security-scan timer (every 6h)? [y/N] " reply + [[ "$reply" =~ ^[Yy]$ ]] && install_scan_timer=true || install_scan_timer=false +fi + +if [[ "$install_scan_timer" == true ]]; then + mkdir -p "$BIN_DIR" "$SYSTEMD_DIR" + cp "$SCAN_SCRIPT_SRC" "$SCAN_SCRIPT_DST" + chmod +x "$SCAN_SCRIPT_DST" + cp "$SCAN_SERVICE_SRC" "$SYSTEMD_DIR/qs-security-scan.service" + cp "$SCAN_TIMER_SRC" "$SYSTEMD_DIR/qs-security-scan.timer" + systemctl --user daemon-reload + systemctl --user enable --now qs-security-scan.timer + echo "Installed $SCAN_SCRIPT_DST and enabled qs-security-scan.timer" +else + echo "Skipped scan timer install. Run with --scan-timer later to add it." +fi diff --git a/qs-security-scan.sh b/qs-security-scan.sh new file mode 100755 index 0000000..205e1f4 --- /dev/null +++ b/qs-security-scan.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# Writes ~/.cache/qs-security-status.json, read by SecurityWidget.qml's +# statusFile FileView. Referenced by the README's systemd timer but not +# previously shipped in the repo. +# +# AUR-Malware is skipped entirely unless something executable already sits +# at QS_SEC_AUR_MALWARE -- omitting the key is safe, the widget only shows +# a section when its own presence probe finds the tool installed. Default +# path matches SecurityWidget.qml's own install button, which clones +# nightdevil00/AUR-Malware -- the upstream Atomic-Arch/AUR-Malware this repo +# originally pointed at is gone (404); nightdevil00's fork ships the same +# check-atomic-arch_new.sh entry point. +set -uo pipefail + +STATUS_FILE="${QS_SEC_STATUS_FILE:-$HOME/.cache/qs-security-status.json}" +AUR_MALWARE_PATH="${QS_SEC_AUR_MALWARE:-$HOME/.local/share/AUR-Malware/check-atomic-arch_new.sh}" +BUMBLEBEE_BIN="${QS_SEC_BUMBLEBEE:-$HOME/.local/bin/bumblebee}" +CATALOG="${QS_SEC_BUMBLEBEE_CATALOG:-$HOME/.local/share/qs-security/threat-intel}" + +aur_json="null" +if [[ -x $AUR_MALWARE_PATH ]]; then + # --json still prints its live colored progress to stdout before the final + # JSON blob, and the script's exit code is always 0 regardless of findings + # -- a plain "last line" / exit-code check always reports "clean" with a + # disclaimer fragment as the summary, silently hiding real findings. The + # JSON itself is the last '{'-only line to EOF. + aur_out=$("$AUR_MALWARE_PATH" --json 2>/dev/null) + aur_json=$(awk '/^\{$/{f=1} f' <<<"$aur_out" | python3 -c ' +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + print("null"); sys.exit() +verdict = d.get("verdict", "") +status = {"CLEAN": "clean", "WARNINGS": "warn", "COMPROMISED": "fail"}.get(verdict, "error") +s = d.get("summary", {}) +fail, warn, total = s.get("fail", 0), s.get("warn", 0), s.get("total", 0) +summary = str(fail) + " failures, " + str(warn) + " warnings out of " + str(total) + " checks" +print(json.dumps({"status": status, "summary": summary})) +') +fi + +bb_json="null" +if [[ -x $BUMBLEBEE_BIN ]]; then + catalog_args=() + [[ -d $CATALOG ]] && catalog_args=(--exposure-catalog "$CATALOG") + scan_out=$("$BUMBLEBEE_BIN" scan --profile baseline "${catalog_args[@]}" 2>/dev/null) + packages=$(grep -c '"record_type":"package"' <<<"$scan_out") + findings=$(grep -c '"record_type":"finding"' <<<"$scan_out") + bb_status=$([[ $findings -gt 0 ]] && echo findings || echo clean) + bb_json=$(python3 -c ' +import json, sys +status, packages, findings = sys.argv[1], sys.argv[2], sys.argv[3] +summary = f"{packages} packages inventoried, {findings} findings against threat-intel catalog" +print(json.dumps({"status": status, "summary": summary})) +' "$bb_status" "$packages" "$findings") +fi + +mkdir -p "$(dirname "$STATUS_FILE")" +python3 -c ' +import json, sys, datetime +aur, bb = json.loads(sys.argv[1]), json.loads(sys.argv[2]) +out = {"checked": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")} +if aur is not None: out["aur_malware"] = aur +if bb is not None: out["bumblebee"] = bb +print(json.dumps(out)) +' "$aur_json" "$bb_json" >"$STATUS_FILE" diff --git a/systemd/qs-security-scan.service b/systemd/qs-security-scan.service new file mode 100644 index 0000000..8425f30 --- /dev/null +++ b/systemd/qs-security-scan.service @@ -0,0 +1,6 @@ +[Unit] +Description=Security scan for omarchy bar + +[Service] +Type=oneshot +ExecStart=%h/.local/bin/qs-security-scan.sh diff --git a/systemd/qs-security-scan.timer b/systemd/qs-security-scan.timer new file mode 100644 index 0000000..c3beba0 --- /dev/null +++ b/systemd/qs-security-scan.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Periodic security scan for omarchy bar + +[Timer] +OnBootSec=2min +OnUnitActiveSec=6h + +[Install] +WantedBy=timers.target