From c193408ccd7147bd933ca256b95d7096a62494e6 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sun, 20 Sep 2026 18:07:34 +0200 Subject: [PATCH] helium: full uBlock Origin (MV2) via Helium, profile cloned, keyring prompt fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chromium 151 removed the last Manifest V2 code paths and the Web Store purged uBlock Origin on 2026-08-31, so ungoogled-chromium 153 cannot load the full blocker any more. Helium (GPL-3, ungoogled-chromium base) bundles uBO 1.75.0 as a component extension and keeps MV2 alive. - installers/install_helium.sh: pacman deps, AUR via yay or makepkg (+validpgpkeys import), /usr/local/bin/helium-browser -> browser-preexec-wrapper so LeechBlock NG and the hosts refresh still apply, --clone-profile with a downgrade check and running-browser refusal, then the verifier. Passed in vmbox from scratch, then host. - installers/verify_helium.py: Xvfb + DevTools exit-code gate — uBO background_page + manifest_version 2, every /etc/chromium/policies key on chrome://policy (Helium reads that dir natively), adblock-tester >= 95. - installers/migrate_helium_passwords_to_basic.py: lightdm autologin leaves gnome-keyring locked, so the 5 v11 logins make Helium prompt on every launch; this re-encrypts them as v10 and only then enables --password-store=basic. - register helium-browser in aur_packages.txt and the periodic browser wrapper/service lists; pylint hook gets websockets + cryptography; codespell learns the vendor name; anthropic 1.6.0 -> 1.7.0 (gate red); oldest evidence artifact pruned to stay under the 60-file cap. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01M8YGXVZdNUG79q5DGsdMm3 --- .pre-commit-config.yaml | 9 +- .../helium-ublock-origin-mv2-2026-09-20.json | 19 ++ .../helium-ublock-origin-mv2-2026-09-20.json | 53 ++++ ...arantine-arch-sysctl-cases-2026-08-22.json | 53 ---- .../fresh-install/aur_packages.txt | 1 + .../installers/install_helium.sh | 182 +++++++++++++ .../migrate_helium_passwords_to_basic.py | 183 +++++++++++++ .../installers/verify_helium.py | 241 +++++++++++++++++ .../lib/periodic_browser.sh | 256 +++++++++--------- .../lib/services_browser.sh | 2 +- meta/requirements-test.txt | 2 +- meta/requirements.txt | 3 +- 12 files changed, 819 insertions(+), 185 deletions(-) create mode 100644 docs/superpowers/contracts/helium-ublock-origin-mv2-2026-09-20.json create mode 100644 docs/superpowers/evidence/helium-ublock-origin-mv2-2026-09-20.json delete mode 100644 docs/superpowers/evidence/quarantine-arch-sysctl-cases-2026-08-22.json create mode 100755 linux_configuration/installers/install_helium.sh create mode 100755 linux_configuration/installers/migrate_helium_passwords_to_basic.py create mode 100755 linux_configuration/installers/verify_helium.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93a373ce..4c065c95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -274,6 +274,11 @@ repos: - anthropic - rich - typer + # linux_configuration/installers: verify_helium drives Helium + # over the DevTools websocket; migrate_helium_passwords_to_basic re-encrypts + # Chromium os_crypt blobs (AES-CBC). + - websockets + - cryptography # Test suites and conftest fixtures are linted separately; they use # patterns (protected-access, missing docstrings, fixture shadowing) # that don't belong in the source-code 10/10 gate. @@ -390,7 +395,9 @@ repos: # `artic` is SearXNG's engine id for the Art Institute of Chicago # (setup_searxng.sh), not a misspelling of "arctic" - renaming it # would stop matching the engine. - - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif,dokument,daa,artic + # `imput` is Helium's vendor (imput.net): its profile dir is + # `~/.config/net.imput.helium` and the installers/*helium* scripts spell it out. + - --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif,dokument,daa,artic,imput exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$) # =========================================================================== diff --git a/docs/superpowers/contracts/helium-ublock-origin-mv2-2026-09-20.json b/docs/superpowers/contracts/helium-ublock-origin-mv2-2026-09-20.json new file mode 100644 index 00000000..c9e07f68 --- /dev/null +++ b/docs/superpowers/contracts/helium-ublock-origin-mv2-2026-09-20.json @@ -0,0 +1,19 @@ +{ + "title": "Helium: full uBlock Origin (MV2) on a Chromium browser, with the Chromium profile cloned", + "objective": "Plain chromium (ungoogled-chromium 153) can no longer load full uBlock Origin: Chromium 151 removed the last MV2 code paths and the Web Store purged uBO on 2026-08-31. Install Helium (GPL-3, ungoogled-chromium base, bundles uBO as a component extension), clone the whole Chromium profile into it, keep the machine's browser wrapper (hosts-blocker refresh + LeechBlock NG) and the /etc/chromium policies in force, and prove all of it by exit code rather than a screenshot.", + "acceptance_criteria": [ + "`verify_helium.py` exits 0 on the real profile: uBO has a background_page target and manifest_version 2 (not uBO Lite); every key in /etc/chromium/policies/managed/*.json appears on chrome://policy; adblock-tester.com scores >= 95/100.", + "`/usr/local/bin/helium-browser` resolves to browser-preexec-wrapper, so a Helium launch from dmenu or a shell gets the same LeechBlock NG --load-extension and hosts refresh as chromium.", + "`~/.config/net.imput.helium/Default` holds the same file list as `~/.config/chromium/Default`; Singleton* lock files are not carried; `~/.config/chromium` is untouched.", + "`install_helium.sh --clone-profile` runs end to end in a pristine vmbox guest (installs its own pacman deps, builds the AUR package without yay via makepkg + PGP key import) and then idempotently on the host.", + "`migrate_helium_passwords_to_basic.py` converts every v11 `logins` row to v10, round-trip-verifies each before writing, backs up `Login Data` first, refuses while Helium runs, and only then appends --password-store=basic to ~/.config/helium-browser-flags.conf so no restart can land between flag-on and rows-converted.", + "helium-browser-bin is listed in fresh-install/aur_packages.txt and helium-browser in the periodic browser wrapper + service-check lists." + ], + "out_of_scope": [ + "Changing the default browser: it is librewolf, not chromium, so the Q4 answer's premise was wrong and the switch is held for an explicit yes.", + "Removing ungoogled-chromium; it stays installed as the fallback.", + "Refreshing the stale installed browser-preexec-wrapper (template in ~/src/system-maintenance has crash logging the host copy lacks).", + "The unrelated uncommitted python_pkg/android_ui + phone_lease work in the tree, which is what drags pytest-coverage below 100% in the full-tree hook run." + ], + "verifier": "verify_helium.py on the host profile (3/3 PASS, score 96) and inside vmbox after a from-scratch install; pre-commit run --files on every staged file; the os_crypt scheme in the migration script validated by decrypting + round-tripping 5 real v10 cookie blobs with the basic key (no keyring touched, no secrets printed)." +} diff --git a/docs/superpowers/evidence/helium-ublock-origin-mv2-2026-09-20.json b/docs/superpowers/evidence/helium-ublock-origin-mv2-2026-09-20.json new file mode 100644 index 00000000..77358958 --- /dev/null +++ b/docs/superpowers/evidence/helium-ublock-origin-mv2-2026-09-20.json @@ -0,0 +1,53 @@ +{ + "intent": "Get full uBlock Origin (Manifest V2, not Lite) back on a FOSS Chromium-based browser after Chromium 151 dropped MV2: install Helium, clone the Chromium profile into it wholesale, keep the wrapper/policy/LeechBlock setup intact, and stop the gnome-keyring unlock prompt that autologin causes.", + "scope": [ + "linux_configuration/installers/{install_helium.sh,verify_helium.py,migrate_helium_passwords_to_basic.py} (new)", + "linux_configuration/fresh-install/aur_packages.txt, periodic_background/lib/{periodic_browser,services_browser}.sh (register helium-browser)", + ".pre-commit-config.yaml (pylint deps websockets+cryptography; codespell ignores the vendor name `imput`)", + "meta/requirements.txt (+cryptography 50.0.1; anthropic 1.6.0 -> 1.7.0 for the freshness gate), meta/requirements-test.txt (anthropic)", + "Non-goals: default-browser switch (current default is librewolf), removing ungoogled-chromium, refreshing the stale host copy of browser-preexec-wrapper" + ], + "changes": [ + "install_helium.sh: pacman deps, AUR install via yay or makepkg (+validpgpkeys import), /usr/local/bin/helium-browser -> browser-preexec-wrapper, --clone-profile with downgrade check (Helium's Chromium base parsed from `--version`) and running-browser refusal, then runs the verifier.", + "verify_helium.py: Xvfb + DevTools (port published via DevToolsActivePort, websockets sync client): asserts uBO background_page + manifest_version 2, every /etc/chromium/policies key on chrome://policy (shadow-DOM text walk), adblock-tester.com >= 95. d3ward was the first choice and is archived/dead.", + "migrate_helium_passwords_to_basic.py: re-encrypts v11 (keyring) `logins` rows as v10 (basic) with the Chromium Linux os_crypt scheme; round-trip check per row, backup first, refuses while Helium runs, appends --password-store=basic to the flags file only after success.", + "Profile clone on the host: rsync ~/.config/chromium -> ~/.config/net.imput.helium minus Singleton*/DevToolsActivePort/Crash Reports/BrowserMetrics*." + ], + "verification": [ + { + "command": "linux_configuration/installers/verify_helium.py", + "result": "pass", + "evidence": "[PASS] uBlock Origin is MV2 (full, not Lite): name=uBlock Origin version=1.75.0 manifest_version=2 background_page=True; [PASS] 7 policies from /etc/chromium/policies/managed applied; [PASS] adblock-tester score >= 95/100: score=96" + }, + { + "command": "vm new helium; vm scp ...; vm run helium './install_helium.sh --clone-profile'", + "result": "pass", + "evidence": "pristine guest: deps installed, helium-browser-bin built via makepkg after gpg --recv-keys, clone + downgrade check ok, verifier 3/3 PASS (0 policies in the guest), score 96" + }, + { + "command": "linux_configuration/installers/install_helium.sh (host)", + "result": "pass", + "evidence": "package already installed; linked /usr/local/bin/helium-browser -> browser-preexec-wrapper; verifier 3/3 PASS" + }, + { + "command": "python: decrypt + encrypt round-trip of 5 real v10 cookie blobs with derive_key(b'peanuts')", + "result": "pass", + "evidence": "5/5 blobs unpad cleanly and round-trip; a wrong key fails PKCS#7 unpadding, so this validates the os_crypt scheme without touching the keyring" + }, + { + "command": "pre-commit run --files ", + "result": "pass", + "evidence": "ruff, ruff-format, pylint (import-error gone once websockets+cryptography were added to the hook), shellcheck, shfmt, codespell, file-length, dependency-freshness all pass; pytest-coverage fails only on python_pkg/android_ui/cli.py 96% and python_pkg/phone_lease 0-65%, which are another session's uncommitted files (not staged here)" + } + ], + "risks": [ + "The 5 keyring-encrypted passwords are unusable in Helium until the user runs migrate_helium_passwords_to_basic.py (it needs the keyring unlocked once); until then the keyring prompt persists.", + "Basic store = fixed key at rest, i.e. anyone with file access reads the passwords; on an autologin box that is already the practical state and matches the existing 36 v10 cookies.", + "adblock-tester.com is a third-party page; if it changes its score text the gate fails closed (score=-1)." + ], + "rollback": [ + "yay -Rns helium-browser-bin; sudo rm /usr/local/bin/helium-browser; rm -rf ~/.config/net.imput.helium ~/.config/helium-browser-flags.conf — ~/.config/chromium was never modified.", + "Passwords: restore Default/'Login Data.pre-basic-' over 'Login Data' in the Helium profile.", + "Validate: chromium starts on its own profile; verify_helium.py exits 2 (binary absent)." + ] +} diff --git a/docs/superpowers/evidence/quarantine-arch-sysctl-cases-2026-08-22.json b/docs/superpowers/evidence/quarantine-arch-sysctl-cases-2026-08-22.json deleted file mode 100644 index 4fccb839..00000000 --- a/docs/superpowers/evidence/quarantine-arch-sysctl-cases-2026-08-22.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "intent": "Restore bt_audio.sh's coverage measurement by taking the arch_sysctl test file out of run_all.sh's glob, after discovering that its presence made the shell coverage gate fail on a lib that is off the allowlist.", - "scope": [ - "linux_configuration/scripts/single_use/fixes/lib/tests/test_arch_sysctl.sh renamed to arch_sysctl_quarantined_cases.sh", - "Non-goal: deleting or weakening any assertion -- all 23 still run and still pass", - "Non-goal: arch_sysctl.sh's allowlist entry, which stays because the lib was never measurable" - ], - "changes": [ - "Renamed the file out of the test_*.sh glob that run_all.sh iterates, so the suite no longer loads it", - "Rewrote its header to record the bisection, the ruled-out causes, how to run the cases directly, and what to re-verify before ever un-quarantining it" - ], - "verification": [ - { - "command": "bash meta/scripts/check_shell_coverage.sh .../fixes/lib/bt_audio.sh (before the rename)", - "result": "fail", - "evidence": "EXIT 1, 'bt_audio.sh below the coverage bar'. bt_audio.sh is OFF the allowlist at 127/127, so commit 7c14cfca had left the gate red on main." - }, - { - "command": "shell_coverage_jail.sh --measure bt_audio.sh, with and without the file in the suite", - "result": "pass", - "evidence": "Without it: bt_audio.sh 127/127 = 100.00% and arch_cpu.sh 57/57 = 100.00%, with every other new test file present. With it: 'kcov instrumented no lines'." - }, - { - "command": "git bisect by measuring bt_audio.sh at three commits", - "result": "pass", - "evidence": "a2b23521 127/127, afe6ac3a 127/127, 7c14cfca broken -- isolating the change to the commit that added this test file." - }, - { - "command": "Independently reverting each lib_test_core.sh change in a throwaway worktree", - "result": "pass", - "evidence": "Removing `export out` alone left bt_audio broken; removing the _t_hide farm cache alone left it broken. Neither is the cause; the test file's presence is." - }, - { - "command": "bash linux_configuration/scripts/single_use/fixes/lib/tests/arch_sysctl_quarantined_cases.sh", - "result": "pass", - "evidence": "arch_sysctl: 23 passed, 0 failed -- the cases are correct and still runnable, just not part of the measured suite." - }, - { - "command": "bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh", - "result": "pass", - "evidence": "All fixes lib test files passed." - } - ], - "risks": [ - "23 assertions no longer run in CI, because the suite runner only picks up test_*.sh. They are a manual invocation away and the header says so.", - "The underlying kcov behaviour is not understood: a single case, or the file with its body neutered, does NOT reproduce it, so the trigger is cumulative rather than one construct. Any future test file added to this suite can do the same thing to another cleared lib.", - "Mitigation for that risk is procedural: re-measure every off-allowlist lib in this directory after adding any test file, not just the new lib." - ], - "rollback": [ - "git mv arch_sysctl_quarantined_cases.sh back to test_arch_sysctl.sh", - "Then re-measure bt_audio.sh through run_all.sh and confirm 127/127 BEFORE committing; the gate will otherwise go red on main again." - ] -} diff --git a/linux_configuration/fresh-install/aur_packages.txt b/linux_configuration/fresh-install/aur_packages.txt index 7d3de425..276b94e5 100644 --- a/linux_configuration/fresh-install/aur_packages.txt +++ b/linux_configuration/fresh-install/aur_packages.txt @@ -1,6 +1,7 @@ local-arch-wiki https://aur.archlinux.org/local-arch-wiki.git visual-studio-code-bin https://aur.archlinux.org/visual-studio-code-bin.git thorium-browser-bin https://aur.archlinux.org/thorium-browser-bin.git +helium-browser-bin https://aur.archlinux.org/helium-browser-bin.git mkinitcpio-git https://aur.archlinux.org/mkinitcpio-git.git yay https://aur.archlinux.org/yay.git http-parser-git https://aur.archlinux.org/http-parser-git.git diff --git a/linux_configuration/installers/install_helium.sh b/linux_configuration/installers/install_helium.sh new file mode 100755 index 00000000..15483be8 --- /dev/null +++ b/linux_configuration/installers/install_helium.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Install Helium (https://github.com/imputnet/helium-linux) — the Chromium +# fork that still runs FULL uBlock Origin (Manifest V2) on Arch. +# +# Why Helium: Chromium 151 removed the last MV2 code paths and Google purged +# uBO from the Web Store on 2026-08-31, so plain chromium / ungoogled-chromium +# can no longer load it. Helium is GPL-3, built on ungoogled-chromium, and +# bundles uBO as a component extension (ID blockjmkbacgjkknlgpkjjiijinjdanf) +# — it is not in the profile, so a cloned profile cannot lose it. +# +# What this does: +# 1. yay -S helium-browser-bin +# 2. routes /usr/local/bin/helium-browser through browser-preexec-wrapper, +# the same pre-exec hook every other browser here gets (hosts-blocker +# refresh + LeechBlock NG via --load-extension). Skipping this would +# silently drop the site blocker when switching from chromium. +# 3. --clone-profile: copies ~/.config/chromium -> ~/.config/net.imput.helium +# (Helium's profile dir), minus lock files. Refuses if the target exists +# or a browser is running (LevelDB copies inconsistently from a live +# process). Helium's Chromium base must be >= the source profile's +# "Last Version" or Chromium treats it as a downgrade; checked here. +# 4. runs verify_helium.py: MV2 uBO present, /etc/chromium/policies applied +# (Helium reads that dir natively — no symlink needed), ad-block score. +# +# Usage: ./install_helium.sh [--clone-profile] [--no-verify] + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly SCRIPT_DIR +readonly PKG="helium-browser-bin" +readonly HELIUM_BIN="/usr/bin/helium-browser" +readonly WRAPPER="/usr/local/bin/browser-preexec-wrapper" +readonly WRAPPER_LINK="/usr/local/bin/helium-browser" +readonly SRC_PROFILE="${XDG_CONFIG_HOME:-$HOME/.config}/chromium" +readonly DST_PROFILE="${XDG_CONFIG_HOME:-$HOME/.config}/net.imput.helium" + +CLONE_PROFILE=false +VERIFY=true + +usage() { + sed -n '2,/^$/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit 0 +} + +# Everything the clone + verify steps fork: rsync, Xvfb, the CDP websocket +# client; base-devel + git only matter on a box without yay (makepkg path). +readonly -a PACMAN_DEPS=(rsync xorg-server-xvfb python-websockets python-cryptography base-devel git) + +validate_requirements() { + command -v pacman >/dev/null 2>&1 || { + error "pacman not found. This script is for Arch Linux." + exit 1 + } + info "Ensuring pacman deps: ${PACMAN_DEPS[*]}" + sudo pacman -S --needed --noconfirm "${PACMAN_DEPS[@]}" +} + +# yay when present (the normal case here); otherwise the same clone+makepkg +# path fresh-install/lib/packages.sh uses, so a bare box works too. +install_from_aur() { + if command -v yay >/dev/null 2>&1; then + yay -S --needed --noconfirm "$PKG" + return + fi + local build_dir="$HOME/sdk/aur/$PKG" + mkdir -p "$HOME/sdk/aur" + [[ -d $build_dir ]] || git clone "https://aur.archlinux.org/$PKG.git" "$build_dir" + # The PKGBUILD verifies the release tarball's signature; yay imports the + # key on its own, bare makepkg does not. + local key + while read -r key; do + gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys "$key" + done < <(cd "$build_dir" && makepkg --printsrcinfo | awk '/validpgpkeys/ {print $3}') + (cd "$build_dir" && makepkg -si --needed --noconfirm) +} + +install_package() { + if pacman -Q "$PKG" >/dev/null 2>&1; then + info "$PKG already installed ($(pacman -Q "$PKG" | cut -d' ' -f2))" + else + info "Installing $PKG from AUR..." + install_from_aur + fi + [[ -x $HELIUM_BIN ]] || { + error "$HELIUM_BIN missing after install" + exit 1 + } +} + +# /usr/local/bin precedes /usr/bin, so `helium-browser` from dmenu/a shell +# resolves to the wrapper, exactly like chromium/firefox do on this machine. +wire_wrapper() { + if [[ ! -x $WRAPPER ]]; then + warn "$WRAPPER not installed (periodic_background/setup_periodic_system.sh) — skipping wrapper link" + return 0 + fi + if [[ -L $WRAPPER_LINK && $(readlink -f "$WRAPPER_LINK") == "$WRAPPER" ]]; then + info "Wrapper link already in place: $WRAPPER_LINK" + return 0 + fi + info "Linking $WRAPPER_LINK -> $WRAPPER" + sudo ln -sf "$WRAPPER" "$WRAPPER_LINK" +} + +# Chromium refuses (or silently resets) a profile written by a newer build. +# `--version` prints "Helium 0.17.2.1 (Chromium 153.0.8010.52)" — the +# parenthesised Chromium number is the one a profile's "Last Version" holds. +profile_downgrade_check() { + local src_ver helium_ver + src_ver="$(cat "$SRC_PROFILE/Last Version" 2>/dev/null || echo 0)" + helium_ver="$("$HELIUM_BIN" --version 2>/dev/null | grep -oE 'Chromium [0-9]+(\.[0-9]+){3}' | cut -d' ' -f2)" + [[ -n $helium_ver ]] || { + error "Could not determine Helium's Chromium base version" + exit 1 + } + if [[ "$(printf '%s\n%s\n' "$src_ver" "$helium_ver" | sort -V | tail -1)" != "$helium_ver" ]]; then + error "Helium $helium_ver is older than the source profile ($src_ver) — cloning would be a downgrade." + exit 1 + fi + info "Profile version $src_ver <= Helium $helium_ver — clone is safe" +} + +clone_profile() { + [[ -d $SRC_PROFILE ]] || { + error "No source profile at $SRC_PROFILE" + exit 1 + } + [[ -e $DST_PROFILE ]] && { + error "$DST_PROFILE already exists — refusing to overwrite. Remove it yourself if you want a re-clone." + exit 1 + } + if pgrep -f '/usr/lib/chromium|ungoogled-chromium|/opt/helium-browser' >/dev/null 2>&1; then + error "A Chromium/Helium process is running — close it first (a live profile copies inconsistently)." + exit 1 + fi + profile_downgrade_check + info "Cloning $SRC_PROFILE -> $DST_PROFILE" + rsync -a \ + --exclude='SingletonLock' --exclude='SingletonSocket' --exclude='SingletonCookie' \ + --exclude='DevToolsActivePort' --exclude='Crash Reports' --exclude='BrowserMetrics*' \ + "$SRC_PROFILE/" "$DST_PROFILE/" + info "Cloned $(du -sh "$DST_PROFILE" | cut -f1)" + warn "Saved passwords are keyring-encrypted under Chromium's own label; check one in Helium before trusting the clone." +} + +run_verify() { + info "Verifying under Xvfb (never the live display)..." + python3 "$SCRIPT_DIR/verify_helium.py" +} + +main() { + validate_requirements + install_package + wire_wrapper + if $CLONE_PROFILE; then clone_profile; fi + if $VERIFY; then run_verify; fi + info "Done. Launch with: helium-browser" +} + +while [[ $# -gt 0 ]]; do + case $1 in + --clone-profile) CLONE_PROFILE=true ;; + --no-verify) VERIFY=false ;; + -h | --help) usage ;; + *) + error "Unknown option: $1" + exit 1 + ;; + esac + shift +done + +main diff --git a/linux_configuration/installers/migrate_helium_passwords_to_basic.py b/linux_configuration/installers/migrate_helium_passwords_to_basic.py new file mode 100755 index 00000000..16d8376f --- /dev/null +++ b/linux_configuration/installers/migrate_helium_passwords_to_basic.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Re-encrypt keyring-bound (v11) Chromium passwords as basic-store (v10). + +Why: lightdm autologin leaves gnome-keyring locked, so a Chromium-family +browser that finds its "Safe Storage" key in the keyring prompts to unlock it +on every launch. Running Helium with ``--password-store=basic`` stops that, +but ``v11`` rows (encrypted with the keyring key) become unreadable — and +Chromium deletes undecryptable logins on read. This converts them first. + +Linux os_crypt scheme (unchanged since Chromium 4x): key = PBKDF2-HMAC-SHA1 +(secret, "saltysalt", 1 iter, 16 B); AES-128-CBC, IV = 16 spaces, PKCS#7; +``v11`` uses the keyring secret, ``v10`` uses the fixed string "peanuts". + +Needs the keyring unlocked ONCE — secret-tool pops the unlock dialog, so run +this yourself, with the browser closed. Never prints a secret. +""" + +from __future__ import annotations + +import argparse +import hashlib +from pathlib import Path +import shutil +import sqlite3 +import subprocess +import sys +import time + +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +DEFAULT_PROFILE = Path.home() / ".config" / "net.imput.helium" / "Default" +KEYRING_SCHEMA = "chrome_libsecret_os_crypt_password_v2" +SALT = b"saltysalt" +IV = b" " * 16 +KEY_LEN = 16 +BASIC_SECRET = b"peanuts" +V10 = b"v10" +V11 = b"v11" +HELIUM_PROCESS_MARK = "/opt/helium-browser-bin" +FLAGS_FILE = Path.home() / ".config" / "helium-browser-flags.conf" +BASIC_FLAG = "--password-store=basic" + + +def say(msg: str, *, err: bool = False) -> None: + """Write one line to stdout (or stderr); explicit so lint autofix cannot drop it.""" + (sys.stderr if err else sys.stdout).write(msg + "\n") + + +def derive_key(secret: bytes) -> bytes: + """Chromium's os_crypt Linux KDF.""" + return hashlib.pbkdf2_hmac("sha1", secret, SALT, 1, KEY_LEN) + + +def decrypt(key: bytes, blob: bytes) -> bytes: + """AES-128-CBC + PKCS#7 strip; ``blob`` is the payload after the 3-byte tag.""" + dec = Cipher(algorithms.AES(key), modes.CBC(IV)).decryptor() + padded = dec.update(blob) + dec.finalize() + unpad = padding.PKCS7(algorithms.AES.block_size).unpadder() + return unpad.update(padded) + unpad.finalize() + + +def encrypt(key: bytes, plain: bytes) -> bytes: + """Inverse of :func:`decrypt`, without the tag.""" + pad = padding.PKCS7(algorithms.AES.block_size).padder() + padded = pad.update(plain) + pad.finalize() + enc = Cipher(algorithms.AES(key), modes.CBC(IV)).encryptor() + return enc.update(padded) + enc.finalize() + + +def keyring_secret(application: str) -> bytes: + """Fetch the browser's Safe Storage secret (this pops the unlock dialog).""" + for attrs in (["application", application], []): + cmd = ["/usr/bin/secret-tool", "lookup", "xdg:schema", KEYRING_SCHEMA, *attrs] + out = subprocess.run(cmd, capture_output=True, check=False).stdout + if out: + return out.rstrip(b"\n") + msg = f"no {KEYRING_SCHEMA} item in the keyring (locked? dialog cancelled?)" + raise SystemExit(msg) + + +def browser_running() -> bool: + """SQLite writes under a live browser are lost or corrupt the DB.""" + res = subprocess.run( + ["/usr/bin/pgrep", "-f", HELIUM_PROCESS_MARK], capture_output=True, check=False + ) + return res.returncode == 0 + + +def migrate( + db: Path, ring_key: bytes, basic_key: bytes, *, dry_run: bool +) -> tuple[int, int]: + """Rewrite every v11 row in ``logins`` as v10; return (converted, already_v10).""" + conn = sqlite3.connect(db) + try: + rows = conn.execute("SELECT id, password_value FROM logins").fetchall() + converted = already = 0 + for row_id, blob in rows: + if blob[:3] == V10: + already += 1 + continue + if blob[:3] != V11: + say(f" row {row_id}: unknown tag {blob[:3]!r}, left alone", err=True) + continue + plain = decrypt(ring_key, blob[3:]) + new_blob = V10 + encrypt(basic_key, plain) + if decrypt(basic_key, new_blob[3:]) != plain: + msg = f"row {row_id}: round-trip mismatch, aborting before any write" + raise SystemExit(msg) + if not dry_run: + conn.execute( + "UPDATE logins SET password_value = ? WHERE id = ?", + (new_blob, row_id), + ) + converted += 1 + if not dry_run: + conn.commit() + finally: + conn.close() + return converted, already + + +def enable_basic_store() -> None: + """Append the flag Helium's wrapper reads — only after the rows are converted. + + Written here rather than by the installer so a restart can never happen + between "flag on" and "rows converted": with the flag on and v11 rows + still present, Chromium would treat them as undecryptable and delete them. + """ + existing = FLAGS_FILE.read_text() if FLAGS_FILE.exists() else "" + if BASIC_FLAG in existing.split(): + return + with FLAGS_FILE.open("a") as fh: + fh.write(f"{BASIC_FLAG}\n") + say(f"added {BASIC_FLAG} to {FLAGS_FILE}") + + +def parse_args() -> argparse.Namespace: + """CLI flags.""" + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument( + "--profile", + type=Path, + default=DEFAULT_PROFILE, + help="profile dir holding 'Login Data'", + ) + parser.add_argument( + "--application", + default="chromium", + help="keyring item's 'application' attribute", + ) + parser.add_argument( + "--dry-run", action="store_true", help="decrypt + verify only, write nothing" + ) + return parser.parse_args() + + +def main() -> int: + """Back up, convert, report counts.""" + args = parse_args() + db = args.profile / "Login Data" + if not db.is_file(): + say(f"no such file: {db}", err=True) + return 2 + if browser_running(): + say("Helium is running — close it first, then rerun.", err=True) + return 3 + ring_key = derive_key(keyring_secret(args.application)) + basic_key = derive_key(BASIC_SECRET) + if not args.dry_run: + backup = db.with_name(f"Login Data.pre-basic-{time.strftime('%Y%m%d-%H%M%S')}") + shutil.copy2(db, backup) + say(f"backup: {backup}") + converted, already = migrate(db, ring_key, basic_key, dry_run=args.dry_run) + verb = "would convert" if args.dry_run else "converted" + say(f"{verb} {converted} v11 -> v10 rows; {already} already v10") + if not args.dry_run: + enable_basic_store() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux_configuration/installers/verify_helium.py b/linux_configuration/installers/verify_helium.py new file mode 100755 index 00000000..051776ea --- /dev/null +++ b/linux_configuration/installers/verify_helium.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Prove Helium ships full uBlock Origin (MV2) and honours the Chromium policies. + +Runs Helium under a private Xvfb display (never the live desktop) and asserts by +exit code: uBO has a ``background_page`` and ``manifest_version: 2`` (full, not +Lite); every ``/etc/chromium/policies/managed`` key shows on ``chrome://policy``; +adblock-tester.com scores >= ``MIN_SCORE``. +""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from http.client import HTTPConnection +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import time +from typing import Self + +from websockets.sync.client import connect + +HELIUM_BIN = "/usr/bin/helium-browser" +HELIUM_BIN_DIR = "/opt/helium-browser-bin" +DEFAULT_PROFILE = str(Path.home() / ".config" / "net.imput.helium") +UBO_ID = "blockjmkbacgjkknlgpkjjiijinjdanf" +POLICY_DIR = Path("/etc/chromium/policies/managed") +ADBLOCK_TEST_URL = "https://adblock-tester.com/" +MIN_SCORE = 95 +HTTP_OK = 200 +DISPLAY = f":{100 + os.getpid() % 900}" # private Xvfb, unique per run +# Walks light DOM and every open shadow root — chrome://policy is Polymer. +DEEP_TEXT_JS = ( + "(() => { const o = []; const w = (n) => { if (n.shadowRoot) w(n.shadowRoot);" + " for (const c of n.childNodes) { if (c.nodeType === 3) o.push(c.textContent);" + " else w(c); } }; w(document); return o.join(' '); })()" +) +Proc = subprocess.Popen[bytes] +Json = list[dict[str, str]] | dict[str, str] + + +@dataclass(frozen=True) +class Check: + """One named pass/fail result with the evidence behind it.""" + + name: str + ok: bool + detail: str + + +def say(msg: str, *, err: bool = False) -> None: + """Write one line; explicit stream write so lint autofix cannot drop it.""" + (sys.stderr if err else sys.stdout).write(msg + "\n") + + +def devtools_json(port: int, method: str, path: str) -> Json: + """One request against Helium's DevTools HTTP endpoint on loopback.""" + conn = HTTPConnection("127.0.0.1", port, timeout=5) + try: + conn.request(method, path) + resp = conn.getresponse() + if resp.status != HTTP_OK: + msg = f"{method} {path} -> HTTP {resp.status}" + raise OSError(msg) + return json.loads(resp.read()) + finally: + conn.close() + + +def wait_for_devtools(profile: str, timeout: float = 30.0) -> int: + """Wait for Chromium to publish its chosen port in /DevToolsActivePort.""" + port_file = Path(profile) / "DevToolsActivePort" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + port = int(port_file.read_text().splitlines()[0]) + devtools_json(port, "GET", "/json/list") + except OSError, ValueError, IndexError: + time.sleep(0.5) + else: + return port + msg = "DevTools never came up" + raise TimeoutError(msg) + + +class Page: + """Minimal CDP client bound to one fresh tab; use as a context manager.""" + + def __init__(self, port: int, url: str, settle: float) -> None: + """Open a tab at ``url``, attach, and give it ``settle`` seconds to render.""" + target = devtools_json(port, "PUT", f"/json/new?{url}") + self._ws = connect(target["webSocketDebuggerUrl"], max_size=None) + self._next_id = 0 + time.sleep(settle) + + def __enter__(self) -> Self: + """Context-manager entry.""" + return self + + def __exit__(self, *_: object) -> None: + """Close the CDP socket.""" + self._ws.close() + + def evaluate(self, expression: str) -> str: + """Run JS in the page and return its string result.""" + self._next_id += 1 + params = {"expression": expression, "returnByValue": True} + msg = {"id": self._next_id, "method": "Runtime.evaluate", "params": params} + self._ws.send(json.dumps(msg)) + while True: + msg = json.loads(self._ws.recv(timeout=30)) + if msg.get("id") == self._next_id: + return str(msg.get("result", {}).get("result", {}).get("value", "")) + + def text(self) -> str: + """Visible text of the page's light DOM.""" + return self.evaluate("document.body.innerText") + + +def check_ubo_mv2(port: int, targets: list[dict[str, str]]) -> Check: + """UBO full runs a persistent background page; uBO Lite/MV3 cannot.""" + ubo = [t for t in targets if UBO_ID in t.get("url", "")] + has_bg_page = any(t.get("type") == "background_page" for t in ubo) + with Page(port, f"chrome-extension://{UBO_ID}/manifest.json", 1.5) as page: + manifest = page.text() + mv = re.search(r'"manifest_version"\s*:\s*(\d)', manifest) + name = re.search(r'"name"\s*:\s*"([^"]+)"', manifest) + version = re.search(r'"version"\s*:\s*"([^"]+)"', manifest) + ok = has_bg_page and mv is not None and mv.group(1) == "2" + detail = ( + f"name={name.group(1) if name else '?'}" + f" version={version.group(1) if version else '?'}" + f" manifest_version={mv.group(1) if mv else '?'} background_page={has_bg_page}" + ) + return Check("uBlock Origin is MV2 (full, not Lite)", ok, detail) + + +def check_policies(port: int) -> Check: + """Every key in every managed policy JSON must be listed on chrome://policy.""" + expected: set[str] = set() + for f in sorted(POLICY_DIR.glob("*.json")): + expected.update(json.loads(f.read_text()).keys()) + with Page(port, "chrome://policy", 2.0) as page: + text = page.evaluate(DEEP_TEXT_JS) + missing = sorted(k for k in expected if k not in text) + return Check( + f"{len(expected)} policies from {POLICY_DIR} applied", + not missing, + f"missing={missing}" if missing else f"all present: {sorted(expected)}", + ) + + +def check_adblock_score(port: int) -> Check: + """adblock-tester.com prints " points out of 100" once its probes finish.""" + score = -1 + with Page(port, ADBLOCK_TEST_URL, 0.5) as page: + for _ in range(40): # up to ~20 s for the probes to finish + m = re.search(r"(\d{1,3}) points out of 100", page.text()) + if m: + score = int(m.group(1)) + break + time.sleep(0.5) + name = f"adblock-tester score >= {MIN_SCORE}/100" + return Check(name, score >= MIN_SCORE, f"score={score}") + + +def launch(profile: str) -> tuple[Proc, Proc]: + """Start Xvfb and Helium; caller must stop both.""" + (Path(profile) / "DevToolsActivePort").unlink(missing_ok=True) + xvfb_cmd = ["/usr/bin/Xvfb", DISPLAY, "-screen", "0", "1280x800x24"] + quiet = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} + xvfb = subprocess.Popen(xvfb_cmd, **quiet) + time.sleep(1.0) + flags = ["--no-first-run", "--disable-gpu", "--remote-allow-origins=*"] + flags += ["--remote-debugging-port=0", f"--user-data-dir={profile}"] + env = {"DISPLAY": DISPLAY, "HOME": str(Path.home()), "PATH": "/usr/bin"} + helium = subprocess.Popen([HELIUM_BIN, *flags, "about:blank"], env=env, **quiet) + return xvfb, helium + + +def helium_running() -> bool: + """True if any Helium process is alive.""" + cmd = ["/usr/bin/pgrep", "-f", HELIUM_BIN_DIR] + return subprocess.run(cmd, check=False, capture_output=True).returncode == 0 + + +def stop(proc: Proc) -> None: + """SIGTERM then SIGKILL after a grace period.""" + proc.terminate() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + +def parse_args() -> argparse.Namespace: + """CLI flags.""" + parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) + parser.add_argument( + "--user-data-dir", + default=DEFAULT_PROFILE, + help="profile to test (default: the real Helium profile)", + ) + return parser.parse_args() + + +def main() -> int: + """Run all checks; exit 1 if any fails.""" + args = parse_args() + if not Path(HELIUM_BIN).exists() or shutil.which("Xvfb") is None: + say(f"need {HELIUM_BIN} and Xvfb", err=True) + return 2 + # A second instance on an open profile hands off to the running one and + # exits, so DevTools would never appear. + if args.user_data_dir == DEFAULT_PROFILE and helium_running(): + say( + "Helium is already running — close it first (or pass --user-data-dir).", + err=True, + ) + return 3 + xvfb, helium = launch(args.user_data_dir) + try: + port = wait_for_devtools(args.user_data_dir) + time.sleep(3.0) # let component extensions finish loading + targets = devtools_json(port, "GET", "/json/list") + checks = (check_ubo_mv2(port, targets), check_policies(port)) + results = [*checks, check_adblock_score(port)] + finally: + stop(helium) + stop(xvfb) + for r in results: + say(f"[{'PASS' if r.ok else 'FAIL'}] {r.name}: {r.detail}") + return 0 if all(r.ok for r in results) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/linux_configuration/periodic_background/lib/periodic_browser.sh b/linux_configuration/periodic_background/lib/periodic_browser.sh index f3b06c82..4c351ea2 100755 --- a/linux_configuration/periodic_background/lib/periodic_browser.sh +++ b/linux_configuration/periodic_background/lib/periodic_browser.sh @@ -8,38 +8,38 @@ # Function to install browser pre-exec wrapper and wire common browser names install_browser_preexec_wrapper() { - echo "" - echo "6.1 Installing Browser Pre-Exec Wrapper..." - echo "=========================================" + echo "" + echo "6.1 Installing Browser Pre-Exec Wrapper..." + echo "=========================================" - local wrapper="/usr/local/bin/browser-preexec-wrapper" - sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ - "$TEMPLATE_BROWSER_WRAPPER" > "$wrapper" - chmod +x "$wrapper" - echo "✓ Installed wrapper: $wrapper" + local wrapper="/usr/local/bin/browser-preexec-wrapper" + sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \ + "$TEMPLATE_BROWSER_WRAPPER" >"$wrapper" + chmod +x "$wrapper" + echo "✓ Installed wrapper: $wrapper" - # Allow passwordless execution of hosts installer for root-only actions - local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd" - if command -v visudo > /dev/null 2>&1; then - echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" > "$sudoers_file" - chmod 440 "$sudoers_file" - # Validate syntax - visudo -c > /dev/null || echo "Warning: sudoers validation returned non-zero" - echo "✓ Sudoers drop-in created: $sudoers_file" - else - echo "visudo not found; skipping sudoers drop-in" - fi + # Allow passwordless execution of hosts installer for root-only actions + local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd" + if command -v visudo >/dev/null 2>&1; then + echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" >"$sudoers_file" + chmod 440 "$sudoers_file" + # Validate syntax + visudo -c >/dev/null || echo "Warning: sudoers validation returned non-zero" + echo "✓ Sudoers drop-in created: $sudoers_file" + else + echo "visudo not found; skipping sudoers drop-in" + fi - # Create symlinks for common browser commands to the wrapper in /usr/local/bin - # This takes precedence over /usr/bin in PATH on most systems. - local browsers=("thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox") - for b in "${browsers[@]}"; do - local link="/usr/local/bin/$b" - ln -sf "$wrapper" "$link" - done - echo "✓ Symlinked wrapper for common browsers in /usr/local/bin" + # Create symlinks for common browser commands to the wrapper in /usr/local/bin + # This takes precedence over /usr/bin in PATH on most systems. + local browsers=("helium-browser" "thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox") + for b in "${browsers[@]}"; do + local link="/usr/local/bin/$b" + ln -sf "$wrapper" "$link" + done + echo "✓ Symlinked wrapper for common browsers in /usr/local/bin" - redirect_app_mode_desktop_entries + redirect_app_mode_desktop_entries } # Chromium "app mode" shortcuts (--app-id=...) are auto-generated with a direct @@ -49,143 +49,143 @@ install_browser_preexec_wrapper() { # without the --disable-gpu-compositing mitigation added on 2026-07-25. # Chromium regenerates these files, so this re-runs on every install. redirect_app_mode_desktop_entries() { - local target_user="${SUDO_USER:-$USER}" - local apps_dir - apps_dir="$(getent passwd "$target_user" | cut -d: -f6)/.local/share/applications" + local target_user="${SUDO_USER:-$USER}" + local apps_dir + apps_dir="$(getent passwd "$target_user" | cut -d: -f6)/.local/share/applications" - [[ -d $apps_dir ]] || return 0 + [[ -d $apps_dir ]] || return 0 - local rewritten=0 f - while IFS= read -r -d '' f; do - if grep -q '^Exec=/opt/' "$f"; then - # Keep only the BASENAME, whatever the nesting depth: - # /opt/thorium-browser/thorium-browser -> /usr/local/bin/thorium-browser - # /opt/google/chrome/google-chrome -> /usr/local/bin/google-chrome - # The greedy .* up to the last slash is what makes depth irrelevant; - # matching a single path segment silently mangles the nested case. - sed -i -E 's|^Exec=/opt/.*/([^/ ]+)|Exec=/usr/local/bin/\1|' "$f" - rewritten=$((rewritten + 1)) - fi - done < <(find "$apps_dir" -maxdepth 1 -type f -name '*.desktop' -print0 2> /dev/null) + local rewritten=0 f + while IFS= read -r -d '' f; do + if grep -q '^Exec=/opt/' "$f"; then + # Keep only the BASENAME, whatever the nesting depth: + # /opt/thorium-browser/thorium-browser -> /usr/local/bin/thorium-browser + # /opt/google/chrome/google-chrome -> /usr/local/bin/google-chrome + # The greedy .* up to the last slash is what makes depth irrelevant; + # matching a single path segment silently mangles the nested case. + sed -i -E 's|^Exec=/opt/.*/([^/ ]+)|Exec=/usr/local/bin/\1|' "$f" + rewritten=$((rewritten + 1)) + fi + done < <(find "$apps_dir" -maxdepth 1 -type f -name '*.desktop' -print0 2>/dev/null) - if ((rewritten > 0)); then - chown "$target_user" "$apps_dir"/*.desktop 2> /dev/null || true - echo "✓ Redirected $rewritten app-mode .desktop entries through the wrapper" - else - echo "✓ No app-mode .desktop entries needed redirecting" - fi + if ((rewritten > 0)); then + chown "$target_user" "$apps_dir"/*.desktop 2>/dev/null || true + echo "✓ Redirected $rewritten app-mode .desktop entries through the wrapper" + else + echo "✓ No app-mode .desktop entries needed redirecting" + fi } # Function to install automatic system update service install_auto_update() { - echo "" - echo "6.2 Installing Automatic System Update..." - echo "=========================================" + echo "" + echo "6.2 Installing Automatic System Update..." + echo "=========================================" - local update_script="/usr/local/bin/auto-system-update.sh" - local update_service="/etc/systemd/system/auto-system-update.service" - local update_timer="/etc/systemd/system/auto-system-update.timer" + local update_script="/usr/local/bin/auto-system-update.sh" + local update_service="/etc/systemd/system/auto-system-update.service" + local update_timer="/etc/systemd/system/auto-system-update.timer" - # Install script from template with user substitution - local actual_user="${SUDO_USER:-$USER}" - sed -e "s|__ACTUAL_USER__|$actual_user|g" \ - "$TEMPLATE_AUTO_UPDATE" > "$update_script" - chmod +x "$update_script" - echo "✓ Installed auto-update script: $update_script (user: $actual_user)" + # Install script from template with user substitution + local actual_user="${SUDO_USER:-$USER}" + sed -e "s|__ACTUAL_USER__|$actual_user|g" \ + "$TEMPLATE_AUTO_UPDATE" >"$update_script" + chmod +x "$update_script" + echo "✓ Installed auto-update script: $update_script (user: $actual_user)" - # Install systemd service and timer from templates - install -m 0644 "$TEMPLATE_AUTO_UPDATE_SVC" "$update_service" - echo "✓ Installed auto-update service: $update_service" + # Install systemd service and timer from templates + install -m 0644 "$TEMPLATE_AUTO_UPDATE_SVC" "$update_service" + echo "✓ Installed auto-update service: $update_service" - install -m 0644 "$TEMPLATE_AUTO_UPDATE_TIMER" "$update_timer" - echo "✓ Installed auto-update timer: $update_timer" + install -m 0644 "$TEMPLATE_AUTO_UPDATE_TIMER" "$update_timer" + echo "✓ Installed auto-update timer: $update_timer" } # Function to enable and start services enable_services() { - echo "" - echo "7. Enabling Services and Timer..." - echo "=================================" + echo "" + echo "7. Enabling Services and Timer..." + echo "=================================" - # Reload systemd daemon - systemctl daemon-reload - echo "✓ Systemd daemon reloaded" + # Reload systemd daemon + systemctl daemon-reload + echo "✓ Systemd daemon reloaded" - # Enable and start the timer - systemctl enable periodic-system-maintenance.timer - systemctl start periodic-system-maintenance.timer - echo "✓ Timer enabled and started" + # Enable and start the timer + systemctl enable periodic-system-maintenance.timer + systemctl start periodic-system-maintenance.timer + echo "✓ Timer enabled and started" - # Enable startup service (but don't start it now) - systemctl enable periodic-system-startup.service - echo "✓ Startup service enabled" + # Enable startup service (but don't start it now) + systemctl enable periodic-system-startup.service + echo "✓ Startup service enabled" - # Enable hosts file monitor service - systemctl enable hosts-file-monitor.service - systemctl start hosts-file-monitor.service - echo "✓ Hosts file monitor service enabled and started" + # Enable hosts file monitor service + systemctl enable hosts-file-monitor.service + systemctl start hosts-file-monitor.service + echo "✓ Hosts file monitor service enabled and started" - # Enable and start auto-update timer - systemctl enable auto-system-update.timer - systemctl start auto-system-update.timer - echo "✓ Auto-update timer enabled and started" + # Enable and start auto-update timer + systemctl enable auto-system-update.timer + systemctl start auto-system-update.timer + echo "✓ Auto-update timer enabled and started" - # Show timer status - echo "" - echo "Timer Status:" - systemctl status periodic-system-maintenance.timer --no-pager -l + # Show timer status + echo "" + echo "Timer Status:" + systemctl status periodic-system-maintenance.timer --no-pager -l - echo "" - echo "Auto-Update Timer Status:" - systemctl status auto-system-update.timer --no-pager -l + echo "" + echo "Auto-Update Timer Status:" + systemctl status auto-system-update.timer --no-pager -l - echo "" - echo "Hosts Monitor Status:" - systemctl status hosts-file-monitor.service --no-pager -l + echo "" + echo "Hosts Monitor Status:" + systemctl status hosts-file-monitor.service --no-pager -l - echo "" - echo "Next scheduled runs:" - systemctl list-timers periodic-system-maintenance.timer auto-system-update.timer --no-pager + echo "" + echo "Next scheduled runs:" + systemctl list-timers periodic-system-maintenance.timer auto-system-update.timer --no-pager } # Function to create log rotation configuration create_log_rotation() { - echo "" - echo "8. Setting up Log Rotation..." - echo "=============================" + echo "" + echo "8. Setting up Log Rotation..." + echo "=============================" - local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance" - install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf" - echo "✓ Installed log rotation configuration from template: $logrotate_conf" + local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance" + install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf" + echo "✓ Installed log rotation configuration from template: $logrotate_conf" } # Function to run initial execution run_initial_execution() { - echo "" - echo "9. Running Initial Execution..." - echo "===============================" + echo "" + echo "9. Running Initial Execution..." + echo "===============================" - local run_initial=true + local run_initial=true - if [[ $INTERACTIVE_MODE == "true" ]]; then - echo "Would you like to run the system maintenance now to test the setup?" - read -p "Run initial execution? (y/N): " -n 1 -r - echo + if [[ $INTERACTIVE_MODE == "true" ]]; then + echo "Would you like to run the system maintenance now to test the setup?" + read -p "Run initial execution? (y/N): " -n 1 -r + echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - run_initial=false - fi - else - echo "Auto-running initial execution to test the setup (use --interactive to prompt)" - fi + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + run_initial=false + fi + else + echo "Auto-running initial execution to test the setup (use --interactive to prompt)" + fi - if [[ $run_initial == "true" ]]; then - echo "Running initial system maintenance..." - /usr/local/bin/periodic-system-maintenance.sh - echo "✓ Initial execution completed" - else - echo "Skipping initial execution" - fi + if [[ $run_initial == "true" ]]; then + echo "Running initial system maintenance..." + /usr/local/bin/periodic-system-maintenance.sh + echo "✓ Initial execution completed" + else + echo "Skipping initial execution" + fi } # Main execution diff --git a/linux_configuration/periodic_background/lib/services_browser.sh b/linux_configuration/periodic_background/lib/services_browser.sh index 0c55b9a9..72604954 100755 --- a/linux_configuration/periodic_background/lib/services_browser.sh +++ b/linux_configuration/periodic_background/lib/services_browser.sh @@ -37,7 +37,7 @@ check_guest_mode_removal() { if [[ $policy_found == false ]]; then # Only flag as issue if a Chromium browser is actually installed - if command -v thorium-browser &>/dev/null || command -v chromium &>/dev/null || command -v google-chrome &>/dev/null || command -v brave-browser &>/dev/null; then + if command -v helium-browser &>/dev/null || command -v thorium-browser &>/dev/null || command -v chromium &>/dev/null || command -v google-chrome &>/dev/null || command -v brave-browser &>/dev/null; then issues+=("No guest mode removal policies found for installed browsers") status="error" else diff --git a/meta/requirements-test.txt b/meta/requirements-test.txt index 3725c94b..d7b22fcd 100644 --- a/meta/requirements-test.txt +++ b/meta/requirements-test.txt @@ -21,7 +21,7 @@ # suite itself fails loudly here on a missing import. # ============================================================================== -anthropic==1.6.0 +anthropic==1.7.0 beautifulsoup4==4.15.0 coverage==7.16.1 defusedxml==0.7.1 diff --git a/meta/requirements.txt b/meta/requirements.txt index 1985a17a..9a005278 100644 --- a/meta/requirements.txt +++ b/meta/requirements.txt @@ -6,7 +6,7 @@ add-trailing-comma==4.0.0 aiohttp==3.14.3 -anthropic==1.6.0 +anthropic==1.7.0 autoflake==2.4.0 autopep8==2.3.2 bandit==1.9.4 @@ -16,6 +16,7 @@ black==26.5.1 bottle==0.13.4 codespell==2.4.3 coverage==7.16.1 +cryptography==50.0.1 darglint==1.8.1 dead==2.1.0 defusedxml==0.7.1