mirror of
https://github.com/kuhyx/scripts.git
synced 2026-07-04 12:03:03 +02:00
feat: hardened scripts
This commit is contained in:
parent
3b71cf33ee
commit
16e5a47321
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@ -41,3 +41,23 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri
|
||||
- Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them.
|
||||
- Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper.
|
||||
- Run `scripts/meta/shell_check.sh` to detect things to fix before committing.
|
||||
## Detailed LLM Documentation
|
||||
|
||||
For in-depth understanding of specific components, see these dedicated guides:
|
||||
|
||||
- **Hosts Guard**: [hosts/guard/README_FOR_LLM.md](../hosts/guard/README_FOR_LLM.md) - Protection layers, canonical copies, path watchers
|
||||
- **Pacman Wrapper**: [scripts/digital_wellbeing/pacman/README_FOR_LLM.md](../scripts/digital_wellbeing/pacman/README_FOR_LLM.md) - Policy files, integrity checks, challenges
|
||||
- **Midnight Shutdown**: [scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md](../scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md) - Schedule protection, timer system
|
||||
- **Compulsive Block**: [scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md](../scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md) - App launch limiting
|
||||
- **Security Analysis**: [docs/SECURITY_HARDENING_ANALYSIS.md](../docs/SECURITY_HARDENING_ANALYSIS.md) - Vulnerabilities and implementation roadmap
|
||||
|
||||
## Digital Wellbeing Components Summary
|
||||
|
||||
| Component | Purpose | Key Files |
|
||||
|-----------|---------|-----------|
|
||||
| Hosts Guard | Block websites via /etc/hosts | `hosts/install.sh`, `hosts/guard/*` |
|
||||
| Pacman Wrapper | Block package installation | `scripts/digital_wellbeing/pacman/*` |
|
||||
| Midnight Shutdown | Auto-shutdown at night | `scripts/digital_wellbeing/setup_midnight_shutdown.sh` |
|
||||
| Compulsive Block | Limit app launches | `scripts/digital_wellbeing/block_compulsive_opening.sh` |
|
||||
| Music Wrapper | Block music during focus | `scripts/digital_wellbeing/youtube-music-wrapper.sh` |
|
||||
| Screen Locker | Require workout to unlock | External: `~/testsAndMisc/python_pkg/screen_locker/` |
|
||||
696
docs/SECURITY_HARDENING_ANALYSIS.md
Normal file
696
docs/SECURITY_HARDENING_ANALYSIS.md
Normal file
@ -0,0 +1,696 @@
|
||||
# Security Hardening Analysis & Implementation Prompt
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document analyzes six digital wellbeing/security scripts and provides a detailed implementation prompt for hardening them against tampering. The analysis is based on thorough code review of the entire codebase.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Current State Analysis
|
||||
|
||||
### 1. `/etc/hosts` Protection System
|
||||
|
||||
**Files involved:**
|
||||
- [hosts/install.sh](../hosts/install.sh) - Main hosts installer
|
||||
- [hosts/guard/setup_hosts_guard.sh](../hosts/guard/setup_hosts_guard.sh) - Guard layer setup
|
||||
- [hosts/guard/enforce-hosts.sh](../hosts/guard/enforce-hosts.sh) - Enforcement script
|
||||
- [hosts/guard/psychological/unlock-hosts.sh](../hosts/guard/psychological/unlock-hosts.sh) - Delayed unlock
|
||||
|
||||
**Current Protection Layers:**
|
||||
1. ✅ Immutable attribute (`chattr +i`)
|
||||
2. ✅ Canonical copy at `/usr/local/share/locked-hosts`
|
||||
3. ✅ Path watcher (`hosts-guard.path`) auto-restores on modification
|
||||
4. ✅ Read-only bind mount (`hosts-bind-mount.service`)
|
||||
5. ✅ Custom entries protection (blocks removal of blocked domains)
|
||||
6. ✅ Shell history suppression for `unlock-hosts` command
|
||||
|
||||
**CRITICAL VULNERABILITY IDENTIFIED:**
|
||||
- ❌ **NO protection for `/etc/nsswitch.conf`** - A user can simply edit nsswitch.conf and remove `files` from the `hosts:` line, completely bypassing ALL /etc/hosts protections without touching the hosts file itself!
|
||||
|
||||
**Example bypass:**
|
||||
```bash
|
||||
# Original: hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
|
||||
# Tampered: hosts: mymachines resolve [!UNAVAIL=return] myhostname dns
|
||||
# Result: /etc/hosts is completely ignored by the system
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Midnight Shutdown System
|
||||
|
||||
**Files involved:**
|
||||
- [scripts/digital_wellbeing/setup_midnight_shutdown.sh](../scripts/digital_wellbeing/setup_midnight_shutdown.sh) (1359 lines)
|
||||
|
||||
**Current Protection Layers:**
|
||||
1. ✅ Immutable attribute on `/etc/shutdown-schedule.conf`
|
||||
2. ✅ Canonical copy at `/usr/local/share/locked-shutdown-schedule.conf`
|
||||
3. ✅ Path watcher restores config if tampered
|
||||
4. ✅ Schedule protection blocks making schedule more lenient
|
||||
5. ✅ Unlock script with psychological delay
|
||||
|
||||
**VULNERABILITIES IDENTIFIED:**
|
||||
- ❌ The unlock script **explicitly tells users how to bypass**: "sudo /usr/local/sbin/unlock-shutdown-schedule"
|
||||
- ❌ The schedule change logic is communicated in the error message
|
||||
- ❌ No protection against stopping/disabling the timer services
|
||||
- ❌ No protection against modifying the check script at `/usr/local/bin/day-specific-shutdown-check.sh`
|
||||
|
||||
---
|
||||
|
||||
### 3. Screen Locker (Python - External Repo)
|
||||
|
||||
**File:** `/home/kuhy/testsAndMisc/python_pkg/screen_locker/screen_lock.py`
|
||||
|
||||
**Current Workout Types:**
|
||||
1. Running - distance, time, pace validation
|
||||
2. Strength - exercises, sets, reps, weights, total calculation
|
||||
3. Table Tennis - duration, sets, points won/lost
|
||||
|
||||
**VULNERABILITIES IDENTIFIED:**
|
||||
- ❌ **Running option too easy to fake** - just enter plausible numbers
|
||||
- ❌ **Table Tennis lacks real verification** - no mathematical cross-check
|
||||
- ❌ Users can close the window via keyboard shortcuts (Alt+F4, etc.)
|
||||
- ❌ The unlock mechanism is too simple once you know the forms
|
||||
- ❌ Shutdown time adjustment is a REWARD for working out (can be exploited)
|
||||
|
||||
---
|
||||
|
||||
### 4. Pacman Wrapper
|
||||
|
||||
**Files involved:**
|
||||
- [scripts/digital_wellbeing/pacman/pacman_wrapper.sh](../scripts/digital_wellbeing/pacman/pacman_wrapper.sh) (823 lines)
|
||||
- [scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt](../scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt)
|
||||
- [scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh](../scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh)
|
||||
|
||||
**Current Protection:**
|
||||
1. ✅ Policy file integrity verification (SHA256)
|
||||
2. ✅ Blocked keywords list
|
||||
3. ✅ Greylist with challenge
|
||||
4. ✅ VirtualBox hardcoded check (cannot bypass via policy files)
|
||||
5. ✅ Steam weekend-only restriction
|
||||
|
||||
**VULNERABILITIES IDENTIFIED:**
|
||||
- ❌ **Google Chrome not blocked** - `google-chrome` and `google-chrome-stable` missing from blocked list
|
||||
- ❌ No automatic LeechBlock installation when browsers are detected
|
||||
- ❌ User can download `.deb`/`.tar.gz` and install manually
|
||||
|
||||
---
|
||||
|
||||
### 5. Block Compulsive Opening
|
||||
|
||||
**File:** [scripts/digital_wellbeing/block_compulsive_opening.sh](../scripts/digital_wellbeing/block_compulsive_opening.sh) (507 lines)
|
||||
|
||||
**Current Behavior:**
|
||||
- Records first open per hour in state file
|
||||
- Blocks subsequent launches within same hour
|
||||
- Shows notification when blocked
|
||||
|
||||
**CRITICAL VULNERABILITY:**
|
||||
- ❌ **App stays running indefinitely** - User can:
|
||||
1. Open app once per hour (allowed)
|
||||
2. Minimize/hide the window
|
||||
3. Keep it running forever in background
|
||||
4. Compulsive checking still happens, just via Alt+Tab instead of launcher
|
||||
|
||||
---
|
||||
|
||||
### 6. YouTube Music Wrapper
|
||||
|
||||
**File:** [scripts/digital_wellbeing/youtube-music-wrapper.sh](../scripts/digital_wellbeing/youtube-music-wrapper.sh)
|
||||
|
||||
**Current Behavior:**
|
||||
- Checks if focus apps (VSCode, games, etc.) are running
|
||||
- Blocks YouTube Music launch if focus app detected
|
||||
|
||||
**REQUESTED ENHANCEMENT:**
|
||||
- When Steam is open → Block ALL browsers, close any open browsers
|
||||
- When browsers open → Block Steam, close Steam if running
|
||||
- This creates mutual exclusion between gaming and browsing
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Language Considerations
|
||||
|
||||
### Shell (Bash) Limitations
|
||||
|
||||
**Pros:**
|
||||
- Native to the system, no dependencies
|
||||
- Direct access to systemd, chattr, filesystem
|
||||
- Fast for simple operations
|
||||
|
||||
**Cons:**
|
||||
- No persistent daemon capability (need systemd for that)
|
||||
- Race conditions in file operations
|
||||
- Complex state management is fragile
|
||||
- No proper event loop for window monitoring
|
||||
- Cannot easily monitor process list in real-time
|
||||
|
||||
### Python Advantages for Certain Tasks
|
||||
|
||||
**Where Python would be better:**
|
||||
1. **Process monitoring daemon** - Watch for Steam/browsers in real-time with proper event loop
|
||||
2. **Window management** - Using `python-xlib` for proper X11 interaction
|
||||
3. **Complex state machines** - Like the screen locker
|
||||
4. **Cross-repo integration** - The screen_lock.py already shows good patterns
|
||||
|
||||
### Recommendation
|
||||
|
||||
| Component | Keep Bash | Move to Python | Reason |
|
||||
|-----------|-----------|----------------|--------|
|
||||
| hosts guard | ✅ | | Simple file ops, systemd integration |
|
||||
| shutdown schedule | ✅ | | Systemd timers, config files |
|
||||
| screen locker | | ✅ Already | Complex UI, state machine |
|
||||
| pacman wrapper | ✅ | | Must intercept pacman |
|
||||
| compulsive block | | ✅ | Needs daemon for auto-close |
|
||||
| music wrapper | | ✅ | Needs real-time process monitoring |
|
||||
|
||||
**New Python Daemon Needed:** A single "digital wellbeing daemon" that:
|
||||
1. Monitors running processes
|
||||
2. Auto-closes apps after timeout
|
||||
3. Enforces Steam/browser mutual exclusion
|
||||
4. Can be controlled via DBus
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Implementation Prompt
|
||||
|
||||
**Use this prompt in a new conversation to implement the changes:**
|
||||
|
||||
---
|
||||
|
||||
### IMPLEMENTATION PROMPT
|
||||
|
||||
```
|
||||
I need to implement comprehensive security hardening for a Linux digital wellbeing system.
|
||||
The codebase is at ~/linux-configuration/ with these components needing changes:
|
||||
|
||||
## 1. HOSTS PROTECTION - nsswitch.conf Guard
|
||||
|
||||
Location: hosts/guard/
|
||||
|
||||
Create a new protection layer for /etc/nsswitch.conf that:
|
||||
- Monitors nsswitch.conf for changes (systemd path watcher)
|
||||
- Ensures the "hosts:" line ALWAYS contains "files" before "dns"
|
||||
- Creates canonical copy at /usr/local/share/locked-nsswitch.conf
|
||||
- Enforces with chattr +i
|
||||
- Add to setup_hosts_guard.sh installer
|
||||
- Must restore automatically if tampered
|
||||
|
||||
The nsswitch.conf protection is CRITICAL because removing "files" from the
|
||||
hosts line completely bypasses /etc/hosts without touching it.
|
||||
|
||||
## 2. MIDNIGHT SHUTDOWN - Silent Denial
|
||||
|
||||
Location: scripts/digital_wellbeing/setup_midnight_shutdown.sh
|
||||
|
||||
Changes needed:
|
||||
- Remove ALL helpful messages about how to bypass (unlock-shutdown-schedule path)
|
||||
- When user tries to make schedule more lenient:
|
||||
- Simply say "Operation not permitted" with NO explanation
|
||||
- Do NOT mention the unlock script
|
||||
- Do NOT explain what's being blocked
|
||||
- Silently restore canonical values
|
||||
- The unlock script should still exist but be undiscoverable
|
||||
- Consider renaming unlock script to an obscure name
|
||||
- Remove the unlock script path from any logs
|
||||
|
||||
## 3. SCREEN LOCKER - External Repo
|
||||
|
||||
Location: ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py
|
||||
|
||||
Changes needed:
|
||||
- REMOVE the "Running" workout option entirely (too easy to fake)
|
||||
- For "Table Tennis":
|
||||
- Require minimum 15 sets played
|
||||
- Add verification: total_points = points_won + points_lost
|
||||
- Require that total_points >= sets_played * 11 (minimum points per set)
|
||||
- Add random math verification question about the scores
|
||||
- Increase submit delay to 60 seconds
|
||||
- For "Strength":
|
||||
- Already has good verification, keep as-is
|
||||
- Add input focus grabbing to prevent Alt+Tab escape
|
||||
- Disable window close keyboard shortcuts
|
||||
|
||||
## 4. PACMAN WRAPPER - Chrome Block + LeechBlock Auto-Install
|
||||
|
||||
Location: scripts/digital_wellbeing/pacman/
|
||||
|
||||
Changes needed to pacman_blocked_keywords.txt:
|
||||
- Add: google-chrome
|
||||
- Add: google-chrome-stable
|
||||
- Add: chromium
|
||||
- Add: ungoogled-chromium
|
||||
|
||||
New behavior in pacman_wrapper.sh:
|
||||
- After ANY browser is detected installed (via pacman -Qq check):
|
||||
- Automatically run install_leechblock.sh if it exists
|
||||
- LeechBlock installer should:
|
||||
- Detect browser type
|
||||
- Install extension with pre-configured blocking rules
|
||||
- Use firefox-addon-install method or chrome native messaging
|
||||
- If LeechBlock installation fails, BLOCK the browser binary (wrap it)
|
||||
|
||||
## 5. BLOCK COMPULSIVE OPENING - Auto-Close Timer
|
||||
|
||||
Location: scripts/digital_wellbeing/block_compulsive_opening.sh
|
||||
|
||||
New behavior:
|
||||
- After app is allowed to open, start a background timer
|
||||
- After 10 minutes, forcefully close the app (pkill)
|
||||
- Show warning notification at 8 minutes ("Closing in 2 minutes")
|
||||
- The wrapper should spawn a detached monitoring process
|
||||
- State tracking: record PID and launch time
|
||||
- Check for zombie PIDs and clean up state
|
||||
|
||||
Implementation approach:
|
||||
```bash
|
||||
# After exec line in wrapper_main, instead of direct exec:
|
||||
launch_with_timer() {
|
||||
local app="$1"
|
||||
local timeout_minutes=10
|
||||
local real_binary="$2"
|
||||
shift 2
|
||||
|
||||
# Launch app in background
|
||||
"$real_binary" "$@" &
|
||||
local app_pid=$!
|
||||
|
||||
# Record state
|
||||
echo "$app_pid $(date +%s)" > "$STATE_DIR/${app}.running"
|
||||
|
||||
# Spawn killer daemon (detached)
|
||||
(
|
||||
sleep $((timeout_minutes * 60))
|
||||
if kill -0 $app_pid 2>/dev/null; then
|
||||
notify "$app" "Session timeout - closing now" critical
|
||||
kill $app_pid 2>/dev/null
|
||||
sleep 2
|
||||
kill -9 $app_pid 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$STATE_DIR/${app}.running"
|
||||
) &
|
||||
disown
|
||||
|
||||
# Wait for app to exit
|
||||
wait $app_pid 2>/dev/null || true
|
||||
}
|
||||
```
|
||||
|
||||
## 6. YOUTUBE MUSIC → STEAM/BROWSER MUTUAL EXCLUSION
|
||||
|
||||
This requires a more sophisticated approach. Create a new Python daemon.
|
||||
|
||||
Location: scripts/digital_wellbeing/focus_mode_daemon.py (new file)
|
||||
|
||||
Behavior:
|
||||
- Run as a systemd user service
|
||||
- Monitor running processes continuously
|
||||
- When Steam (steam_app_* or steam game processes) detected:
|
||||
- Kill any running browsers (firefox, chrome, brave, etc.)
|
||||
- Block browser launches (via wrapper modification or DBus signal)
|
||||
- Show notification: "Gaming mode active - browsers disabled"
|
||||
- When any browser detected:
|
||||
- Kill Steam processes
|
||||
- Block Steam launches
|
||||
- Show notification: "Browsing mode active - Steam disabled"
|
||||
- Mutual exclusion: whichever started first "wins"
|
||||
- The youtube-music-wrapper.sh should also check for this daemon's signals
|
||||
|
||||
## ADDITIONAL REQUIREMENTS
|
||||
|
||||
1. All changes must be idempotent (can re-run safely)
|
||||
2. All protection mechanisms should fail-closed (if service dies, restrictions remain)
|
||||
3. Log all tampering attempts to /var/log/digital-wellbeing-guard.log
|
||||
4. Create a single test script that verifies all protections work
|
||||
5. Update the .github/copilot-instructions.md with the new components
|
||||
|
||||
## FILES TO CREATE/MODIFY
|
||||
|
||||
New files:
|
||||
- hosts/guard/nsswitch-guard.path
|
||||
- hosts/guard/nsswitch-guard.service
|
||||
- hosts/guard/enforce-nsswitch.sh
|
||||
- scripts/digital_wellbeing/focus_mode_daemon.py
|
||||
- scripts/digital_wellbeing/install_focus_mode_daemon.sh
|
||||
- tests/test_security_hardening.sh
|
||||
|
||||
Modified files:
|
||||
- hosts/guard/setup_hosts_guard.sh (add nsswitch protection)
|
||||
- scripts/digital_wellbeing/setup_midnight_shutdown.sh (remove helpful messages)
|
||||
- scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt (add chrome)
|
||||
- scripts/digital_wellbeing/pacman/pacman_wrapper.sh (leechblock auto-install)
|
||||
- scripts/digital_wellbeing/block_compulsive_opening.sh (auto-close timer)
|
||||
- scripts/digital_wellbeing/youtube-music-wrapper.sh (daemon integration)
|
||||
|
||||
External repo (separate changes):
|
||||
- ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py (remove running, harden table tennis)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Agent Personas
|
||||
|
||||
### Agent: Hosts Guard Expert
|
||||
|
||||
```
|
||||
You are an expert on the linux-configuration hosts guard system. You understand:
|
||||
|
||||
FILES YOU KNOW:
|
||||
- hosts/install.sh - Downloads StevenBlack hosts, adds custom entries, protects with chattr
|
||||
- hosts/guard/setup_hosts_guard.sh - Installs all guard layers (path watcher, bind mount, unlock script)
|
||||
- hosts/guard/enforce-hosts.sh - Called when tampering detected, restores from canonical
|
||||
- hosts/guard/psychological/unlock-hosts.sh - 45-second delay, logs reason, opens editor
|
||||
- hosts/guard/hosts-guard.path/.service - Systemd path watcher
|
||||
- hosts/guard/hosts-bind-mount.service - Read-only bind mount
|
||||
- hosts/guard/pacman-hooks/*.sh - Pre/post transaction hooks for pacman
|
||||
|
||||
KEY CONCEPTS:
|
||||
- Canonical copy at /usr/local/share/locked-hosts
|
||||
- Custom entries state at /etc/hosts.custom-entries.state
|
||||
- Multi-layer defense: chattr + path watcher + bind mount
|
||||
- Shell history suppression for unlock commands
|
||||
|
||||
COMMON TASKS:
|
||||
- Adding new blocked domains: Edit hosts/install.sh heredoc section
|
||||
- Temporarily allowing edits: sudo /usr/local/sbin/unlock-hosts
|
||||
- Checking status: lsattr /etc/hosts, systemctl status hosts-guard.path
|
||||
|
||||
GOTCHAS:
|
||||
- Must run hosts/install.sh BEFORE setup_hosts_guard.sh
|
||||
- Removing custom entries is blocked by protection mechanism
|
||||
- nsswitch.conf bypass is currently unprotected (needs fix)
|
||||
```
|
||||
|
||||
### Agent: Shutdown Schedule Expert
|
||||
|
||||
```
|
||||
You are an expert on the midnight shutdown system. You understand:
|
||||
|
||||
FILES YOU KNOW:
|
||||
- scripts/digital_wellbeing/setup_midnight_shutdown.sh - Main installer (1300+ lines)
|
||||
- /etc/shutdown-schedule.conf - Runtime config (MON_WED_HOUR, THU_SUN_HOUR, MORNING_END_HOUR)
|
||||
- /usr/local/share/locked-shutdown-schedule.conf - Canonical protected copy
|
||||
- /usr/local/bin/day-specific-shutdown-check.sh - Checks if in shutdown window
|
||||
- /usr/local/bin/day-specific-shutdown-manager.sh - Status/management
|
||||
- /etc/systemd/system/day-specific-shutdown.timer/.service - Systemd timer
|
||||
- /etc/systemd/system/shutdown-schedule-guard.path/.service - Config protection
|
||||
|
||||
KEY CONCEPTS:
|
||||
- Day-specific windows: Mon-Wed vs Thu-Sun have different hours
|
||||
- Making schedule STRICTER (earlier) = allowed without delay
|
||||
- Making schedule MORE LENIENT (later) = blocked or requires unlock
|
||||
- MORNING_END_HOUR cannot be lowered (would shorten window)
|
||||
- Monitor service re-enables timer if user disables it
|
||||
|
||||
PROTECTION LAYERS:
|
||||
1. Script checks canonical config, blocks lenient changes
|
||||
2. Config file has chattr +i
|
||||
3. Path watcher restores if file modified
|
||||
4. Canonical copy takes precedence
|
||||
|
||||
INTEGRATION:
|
||||
- i3blocks shutdown_countdown.sh reads the config
|
||||
- screen_lock.py can adjust shutdown time (reward/punishment)
|
||||
```
|
||||
|
||||
### Agent: Pacman Wrapper Expert
|
||||
|
||||
```
|
||||
You are an expert on the pacman wrapper security system. You understand:
|
||||
|
||||
FILES YOU KNOW:
|
||||
- scripts/digital_wellbeing/pacman/pacman_wrapper.sh - Main wrapper (823 lines)
|
||||
- scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh - Backs up real pacman
|
||||
- scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt - Always blocked
|
||||
- scripts/digital_wellbeing/pacman/pacman_whitelist.txt - Exceptions to keywords
|
||||
- scripts/digital_wellbeing/pacman/pacman_greylist.txt - Challenge required
|
||||
- scripts/digital_wellbeing/pacman/words.txt - Word scramble challenge words
|
||||
- /var/lib/pacman-wrapper/policy.sha256 - Integrity checksums
|
||||
|
||||
KEY CONCEPTS:
|
||||
- Real pacman at /usr/bin/pacman.orig, wrapper symlinked to /usr/bin/pacman
|
||||
- Policy integrity verification via SHA256 before ANY operation
|
||||
- Three tiers: blocked (always denied), greylist (challenge), whitelist (bypass)
|
||||
- VirtualBox check is HARDCODED (cannot bypass via policy files)
|
||||
- Steam is weekend-only with word scramble challenge
|
||||
|
||||
POLICY ENFORCEMENT:
|
||||
1. Load policy lists from text files
|
||||
2. Verify integrity hashes match
|
||||
3. Check if package matches blocked keywords (unless whitelisted)
|
||||
4. Check if greylisted (requires challenge)
|
||||
5. After transaction, remove any blocked packages that got installed
|
||||
|
||||
HOSTS INTEGRATION:
|
||||
- Calls /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh before transaction
|
||||
- Calls pacman-post-relock-hosts.sh after transaction
|
||||
- Enforces VirtualBox hosts sharing if vbox detected
|
||||
|
||||
MAINTENANCE INTEGRATION:
|
||||
- Auto-runs setup_periodic_system.sh if maintenance services missing
|
||||
```
|
||||
|
||||
### Agent: Compulsive Opening Blocker Expert
|
||||
|
||||
```
|
||||
You are an expert on the block_compulsive_opening.sh script. You understand:
|
||||
|
||||
FILES YOU KNOW:
|
||||
- scripts/digital_wellbeing/block_compulsive_opening.sh - Main script (507 lines)
|
||||
- /usr/local/bin/block-compulsive-opening.sh - Installed location
|
||||
- ~/.local/state/compulsive-block/*.lastopen - Per-app state files
|
||||
- ~/.local/state/compulsive-block/compulsive-block.log - Activity log
|
||||
- /etc/pacman.d/hooks/95-compulsive-block-rewrap.hook - Auto-rewrap hook
|
||||
|
||||
MANAGED APPS:
|
||||
- beeper → /opt/beeper/beepertexts
|
||||
- signal-desktop → /usr/lib/signal-desktop/signal-desktop
|
||||
- discord → /opt/discord/Discord
|
||||
|
||||
KEY CONCEPTS:
|
||||
- Wrapper replaces /usr/bin/<app>, original saved as .orig or SYMLINK: marker
|
||||
- Hour-based tracking: YYYY-MM-DD-HH format
|
||||
- First launch per hour allowed, subsequent launches blocked
|
||||
- Pacman hook re-installs wrappers after package updates
|
||||
|
||||
WRAPPER FLOW:
|
||||
1. wrapper_main() called with app name
|
||||
2. Check was_opened_this_hour()
|
||||
3. If yes: block_app() + notification + exit 1
|
||||
4. If no: record_opening() + exec real binary
|
||||
|
||||
LIMITATION (needs fix):
|
||||
- Once app is launched, it can run indefinitely
|
||||
- User can minimize and keep checking via Alt+Tab
|
||||
- Needs auto-close timer functionality
|
||||
```
|
||||
|
||||
### Agent: Screen Locker Expert
|
||||
|
||||
```
|
||||
You are an expert on the screen_lock.py workout locker. You understand:
|
||||
|
||||
FILE LOCATION: ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py (1261 lines)
|
||||
|
||||
PURPOSE:
|
||||
- Full-screen lock requiring workout verification to unlock
|
||||
- Integrates with shutdown schedule system
|
||||
|
||||
WORKOUT TYPES:
|
||||
1. Running: distance, time, pace with cross-validation
|
||||
2. Strength: exercises, sets, reps, weights with total calculation
|
||||
3. Table Tennis: duration, sets, points won/lost
|
||||
4. Sick Day: 2-minute wait, shutdown moved 1.5h earlier
|
||||
|
||||
KEY FEATURES:
|
||||
- 30-second delay before submit button enabled
|
||||
- Cross-validation (e.g., pace = time / distance)
|
||||
- 15% tolerance on calculated values
|
||||
- Demo mode (10s lockout) vs Production mode (30min lockout)
|
||||
- JSON workout log stored in same directory
|
||||
|
||||
SHUTDOWN INTEGRATION:
|
||||
- _adjust_shutdown_time_earlier() - sick day penalty
|
||||
- _adjust_shutdown_time_later() - workout reward (+1.5h)
|
||||
- Uses adjust_shutdown_schedule.sh helper script
|
||||
- Sick day state tracked in sick_day_state.json
|
||||
|
||||
SECURITY CONCERNS (needs fix):
|
||||
- Running option too easy to fake
|
||||
- Table tennis lacks rigorous validation
|
||||
- Window can potentially be closed via keyboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 5: LLM README Files
|
||||
|
||||
These should be created in the respective directories:
|
||||
|
||||
### [hosts/guard/README_FOR_LLM.md](to be created)
|
||||
|
||||
```markdown
|
||||
# Hosts Guard System - LLM Reference
|
||||
|
||||
## Purpose
|
||||
Prevent tampering with /etc/hosts to maintain website blocking.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
/etc/hosts (immutable) ←── canonical (/usr/local/share/locked-hosts)
|
||||
↑
|
||||
path watcher detects changes
|
||||
↓
|
||||
enforce-hosts.sh restores
|
||||
```
|
||||
|
||||
## Critical Files
|
||||
| File | Purpose | Protected By |
|
||||
|------|---------|--------------|
|
||||
| /etc/hosts | Actual hosts file | chattr +i, bind mount |
|
||||
| /usr/local/share/locked-hosts | Canonical copy | chattr +i |
|
||||
| /etc/hosts.custom-entries.state | Tracks blocked domains | chattr +i |
|
||||
|
||||
## Commands to Know
|
||||
```bash
|
||||
# Check protection status
|
||||
lsattr /etc/hosts
|
||||
systemctl status hosts-guard.path hosts-bind-mount.service
|
||||
|
||||
# Legitimate edit (with delay)
|
||||
sudo /usr/local/sbin/unlock-hosts
|
||||
|
||||
# Reinstall/repair
|
||||
sudo ~/linux-configuration/hosts/install.sh
|
||||
sudo ~/linux-configuration/hosts/guard/setup_hosts_guard.sh
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
- Edit /etc/nsswitch.conf (bypasses hosts entirely)
|
||||
- Stop hosts-guard.path without understanding consequences
|
||||
- Remove entries from install.sh without state file cleanup
|
||||
```
|
||||
|
||||
### [scripts/digital_wellbeing/pacman/README_FOR_LLM.md](to be created)
|
||||
|
||||
```markdown
|
||||
# Pacman Wrapper - LLM Reference
|
||||
|
||||
## Purpose
|
||||
Intercept pacman to enforce package installation policies.
|
||||
|
||||
## Architecture
|
||||
```
|
||||
/usr/bin/pacman (symlink) → pacman_wrapper.sh
|
||||
↓
|
||||
/usr/bin/pacman.orig (real)
|
||||
```
|
||||
|
||||
## Policy Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| pacman_blocked_keywords.txt | Substring match = always blocked |
|
||||
| pacman_whitelist.txt | Exact names that bypass blocking |
|
||||
| pacman_greylist.txt | Requires challenge to install |
|
||||
| words.txt | Word scramble challenge source |
|
||||
|
||||
## Hardcoded Checks (cannot bypass via files)
|
||||
- VirtualBox → security challenge + hosts enforcement
|
||||
- Steam → weekend-only + word scramble
|
||||
|
||||
## Integration Points
|
||||
1. Hosts guard (pre/post hooks)
|
||||
2. Periodic maintenance (auto-setup if missing)
|
||||
3. VirtualBox hosts enforcement
|
||||
|
||||
## Adding Blocks
|
||||
```bash
|
||||
# Edit the blocked keywords file
|
||||
echo "newpackage" >> pacman_blocked_keywords.txt
|
||||
|
||||
# Re-run installer to update checksums
|
||||
sudo ./install_pacman_wrapper.sh
|
||||
```
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 6: Test Script Template
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# tests/test_security_hardening.sh
|
||||
# Verify all security mechanisms are working
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
test_result() {
|
||||
local name="$1"
|
||||
local result="$2"
|
||||
if [[ $result == "pass" ]]; then
|
||||
echo "✅ PASS: $name"
|
||||
((PASS++))
|
||||
else
|
||||
echo "❌ FAIL: $name"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
# Test 1: /etc/hosts is immutable
|
||||
if lsattr /etc/hosts 2>/dev/null | grep -q '^....i'; then
|
||||
test_result "/etc/hosts is immutable" "pass"
|
||||
else
|
||||
test_result "/etc/hosts is immutable" "fail"
|
||||
fi
|
||||
|
||||
# Test 2: hosts-guard.path is active
|
||||
if systemctl is-active --quiet hosts-guard.path; then
|
||||
test_result "hosts-guard.path is active" "pass"
|
||||
else
|
||||
test_result "hosts-guard.path is active" "fail"
|
||||
fi
|
||||
|
||||
# Test 3: shutdown-schedule.conf is immutable
|
||||
if lsattr /etc/shutdown-schedule.conf 2>/dev/null | grep -q '^....i'; then
|
||||
test_result "/etc/shutdown-schedule.conf is immutable" "pass"
|
||||
else
|
||||
test_result "/etc/shutdown-schedule.conf is immutable" "fail"
|
||||
fi
|
||||
|
||||
# Test 4: pacman wrapper is installed
|
||||
if [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]]; then
|
||||
test_result "pacman wrapper installed" "pass"
|
||||
else
|
||||
test_result "pacman wrapper installed" "fail"
|
||||
fi
|
||||
|
||||
# Test 5: google-chrome is blocked
|
||||
if grep -qi "google-chrome" ~/linux-configuration/scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt; then
|
||||
test_result "google-chrome in blocked list" "pass"
|
||||
else
|
||||
test_result "google-chrome in blocked list" "fail"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
echo "=========================================="
|
||||
|
||||
exit $FAIL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
This analysis identifies critical vulnerabilities and provides a comprehensive implementation prompt. The most urgent issues are:
|
||||
|
||||
1. **nsswitch.conf bypass** - Completely unprotected, defeats all hosts protections
|
||||
2. **Information disclosure** - Shutdown system tells users how to bypass
|
||||
3. **App lifetime** - Compulsive blockers don't limit session duration
|
||||
4. **Browser gaps** - Chrome not blocked, no LeechBlock auto-install
|
||||
|
||||
The implementation prompt above should be used in a focused coding session to address all issues systematically.
|
||||
205
hosts/guard/README_FOR_LLM.md
Normal file
205
hosts/guard/README_FOR_LLM.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Hosts Guard System - LLM Reference Guide
|
||||
|
||||
> **For AI assistants**: This document explains how the hosts guard system works so you can make correct modifications.
|
||||
|
||||
## System Purpose
|
||||
|
||||
Prevent tampering with `/etc/hosts` to maintain website blocking (YouTube, social media, etc.) as part of a digital wellbeing system.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PROTECTION LAYERS │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Layer 1: Immutable Attribute │
|
||||
│ ───────────────────────────── │
|
||||
│ /etc/hosts has chattr +i (cannot be modified even by root) │
|
||||
│ │
|
||||
│ Layer 2: Canonical Copy │
|
||||
│ ─────────────────────── │
|
||||
│ /usr/local/share/locked-hosts contains the "true" version │
|
||||
│ If /etc/hosts differs, it gets overwritten from this copy │
|
||||
│ │
|
||||
│ Layer 3: Path Watcher (systemd) │
|
||||
│ ────────────────────────────── │
|
||||
│ hosts-guard.path watches /etc/hosts for ANY change │
|
||||
│ hosts-guard.service runs enforce-hosts.sh when triggered │
|
||||
│ │
|
||||
│ Layer 4: Read-Only Bind Mount │
|
||||
│ ──────────────────────────── │
|
||||
│ hosts-bind-mount.service mounts /etc/hosts read-only │
|
||||
│ Even if chattr is removed, write operations fail │
|
||||
│ │
|
||||
│ Layer 5: Custom Entries Protection │
|
||||
│ ───────────────────────────────── │
|
||||
│ /etc/hosts.custom-entries.state tracks blocked domains │
|
||||
│ Prevents removal of domains from install.sh │
|
||||
│ │
|
||||
│ Layer 6: nsswitch.conf Protection (NEW) │
|
||||
│ ─────────────────────────────────────── │
|
||||
│ Prevents bypass via /etc/nsswitch.conf manipulation │
|
||||
│ Ensures "files" always appears in hosts: line before "dns" │
|
||||
│ nsswitch-guard.path watches for changes │
|
||||
│ Canonical copy at /usr/local/share/locked-nsswitch.conf │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose | Protection |
|
||||
|------|---------|------------|
|
||||
| `/etc/hosts` | Active hosts file | chattr +i, bind mount |
|
||||
| `/usr/local/share/locked-hosts` | Canonical source of truth | chattr +i |
|
||||
| `/etc/hosts.custom-entries.state` | Tracks custom blocked domains | chattr +i |
|
||||
| `/etc/hosts.stevenblack` | Cached upstream hosts file | None |
|
||||
| `/etc/nsswitch.conf` | Name service switch config | chattr +i, path watcher |
|
||||
| `/usr/local/share/locked-nsswitch.conf` | Canonical nsswitch copy | chattr +i |
|
||||
| `/usr/local/sbin/enforce-hosts.sh` | Restoration script | File permissions |
|
||||
| `/usr/local/sbin/enforce-nsswitch.sh` | nsswitch enforcement | File permissions |
|
||||
| `/usr/local/sbin/unlock-hosts` | Psychological unlock script | File permissions |
|
||||
| `/etc/systemd/system/hosts-guard.path` | Path watcher unit | systemd |
|
||||
| `/etc/systemd/system/hosts-guard.service` | Enforcement service | systemd |
|
||||
| `/etc/systemd/system/hosts-bind-mount.service` | RO bind mount | systemd |
|
||||
| `/etc/systemd/system/nsswitch-guard.path` | nsswitch watcher | systemd |
|
||||
| `/etc/systemd/system/nsswitch-guard.service` | nsswitch enforce | systemd |
|
||||
|
||||
## Key Scripts
|
||||
|
||||
### hosts/install.sh
|
||||
- Downloads StevenBlack hosts list (cached at `/etc/hosts.stevenblack`)
|
||||
- Adds custom blocking entries (YouTube, etc.)
|
||||
- Comments out allowed sites (4chan, Facebook)
|
||||
- Runs protection check for custom entries
|
||||
- Sets up initial immutable attribute
|
||||
|
||||
### hosts/guard/setup_hosts_guard.sh
|
||||
Installs all protection layers:
|
||||
- Creates canonical snapshot
|
||||
- Installs enforce-hosts.sh and unlock-hosts scripts
|
||||
- Enables systemd path watcher
|
||||
- Enables bind mount service
|
||||
- Installs shell history suppression hooks
|
||||
|
||||
### hosts/guard/enforce-hosts.sh
|
||||
Called when tampering detected:
|
||||
```bash
|
||||
# Compares /etc/hosts to canonical
|
||||
# If different: restores from canonical, logs event
|
||||
# Re-applies chattr +i
|
||||
```
|
||||
|
||||
### hosts/guard/psychological/unlock-hosts.sh
|
||||
Legitimate edit workflow:
|
||||
1. Prompts for reason (logged)
|
||||
2. Stops protection services
|
||||
3. Waits 45 seconds (cooling off)
|
||||
4. Opens editor
|
||||
5. Updates canonical if changes made
|
||||
6. Re-enables all protections
|
||||
|
||||
## Pacman Integration
|
||||
|
||||
The pacman wrapper calls these hooks during package transactions:
|
||||
- `/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh` - Before transaction
|
||||
- `/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh` - After transaction
|
||||
|
||||
These temporarily unlock hosts for package manager operations.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a New Blocked Domain
|
||||
|
||||
1. Edit `hosts/install.sh`
|
||||
2. Find the heredoc section after `# Custom blocking entries`
|
||||
3. Add line: `0.0.0.0 newdomain.com`
|
||||
4. Run: `sudo ~/linux-configuration/hosts/install.sh`
|
||||
|
||||
```bash
|
||||
# Example: Block example.com
|
||||
# In hosts/install.sh, add to heredoc:
|
||||
0.0.0.0 example.com
|
||||
0.0.0.0 www.example.com
|
||||
```
|
||||
|
||||
### Allowing a Previously Blocked Domain
|
||||
|
||||
**This is intentionally difficult.** You must:
|
||||
1. Remove entry from install.sh heredoc
|
||||
2. Remove protection: `sudo chattr -i /etc/hosts.custom-entries.state`
|
||||
3. Edit state file to remove domain
|
||||
4. Re-run install.sh
|
||||
|
||||
### Checking Protection Status
|
||||
|
||||
```bash
|
||||
# Check immutable attribute
|
||||
lsattr /etc/hosts
|
||||
# Should show: ----i--------e-- /etc/hosts
|
||||
|
||||
# Check services
|
||||
systemctl status hosts-guard.path hosts-guard.service hosts-bind-mount.service
|
||||
|
||||
# Check canonical exists
|
||||
ls -la /usr/local/share/locked-hosts
|
||||
```
|
||||
|
||||
### Legitimate Editing
|
||||
|
||||
```bash
|
||||
sudo /usr/local/sbin/unlock-hosts
|
||||
# Enter reason when prompted
|
||||
# Wait 45 seconds
|
||||
# Edit in your $EDITOR
|
||||
# Changes auto-saved to canonical
|
||||
```
|
||||
|
||||
## nsswitch.conf Protection (Layer 6)
|
||||
|
||||
**Why this matters:** A user could bypass ALL /etc/hosts protections by simply editing `/etc/nsswitch.conf` and removing `files` from the `hosts:` line. This protection layer prevents that.
|
||||
|
||||
### How it works:
|
||||
- `nsswitch-guard.path` watches `/etc/nsswitch.conf` for changes
|
||||
- `nsswitch-guard.service` runs `enforce-nsswitch.sh` when triggered
|
||||
- Canonical copy stored at `/usr/local/share/locked-nsswitch.conf`
|
||||
- Validates that `hosts:` line contains `files` before `dns`
|
||||
- Auto-restores from canonical if tampered
|
||||
|
||||
### Check nsswitch protection status:
|
||||
```bash
|
||||
lsattr /etc/nsswitch.conf
|
||||
systemctl status nsswitch-guard.path
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot modify /etc/hosts"
|
||||
This is expected! Use the unlock script:
|
||||
```bash
|
||||
sudo /usr/local/sbin/unlock-hosts
|
||||
```
|
||||
|
||||
### Path watcher not running
|
||||
```bash
|
||||
sudo systemctl start hosts-guard.path
|
||||
sudo systemctl enable hosts-guard.path
|
||||
```
|
||||
|
||||
### Bind mount preventing access
|
||||
```bash
|
||||
# Temporarily disable (not recommended)
|
||||
sudo systemctl stop hosts-bind-mount.service
|
||||
```
|
||||
|
||||
### Custom entries protection blocking install
|
||||
The protection mechanism detected you're trying to remove previously blocked domains. This is intentional. To proceed, manually edit the state file (see "Allowing a Previously Blocked Domain").
|
||||
|
||||
## DO NOT
|
||||
|
||||
1. ❌ Edit `/etc/nsswitch.conf` to bypass hosts (this defeats the purpose)
|
||||
2. ❌ Stop `hosts-guard.path` without understanding consequences
|
||||
3. ❌ Delete `/usr/local/share/locked-hosts` (breaks restoration)
|
||||
4. ❌ Remove entries from install.sh without updating state file
|
||||
5. ❌ Use `chattr -i` without going through unlock-hosts
|
||||
103
hosts/guard/enforce-nsswitch.sh
Normal file
103
hosts/guard/enforce-nsswitch.sh
Normal file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Template guard script to enforce canonical /etc/nsswitch.conf
|
||||
# Ensures "hosts:" line always contains "files" before "dns"
|
||||
# This prevents bypassing /etc/hosts by removing "files" from nsswitch.conf
|
||||
# Installed to /usr/local/sbin/enforce-nsswitch.sh by setup_hosts_guard.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CANONICAL_SOURCE="/usr/local/share/locked-nsswitch.conf"
|
||||
TARGET="/etc/nsswitch.conf"
|
||||
LOG_FILE="/var/log/nsswitch-guard.log"
|
||||
|
||||
log() {
|
||||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2
|
||||
}
|
||||
|
||||
# Function to validate that "hosts:" line has correct format
|
||||
# Must contain "files" before "dns" (or "dns" not present)
|
||||
validate_hosts_line() {
|
||||
local line="$1"
|
||||
|
||||
# Check if "files" is present
|
||||
if ! echo "$line" | grep -qw "files"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# If dns is present, files must come before it
|
||||
if echo "$line" | grep -qw "dns"; then
|
||||
local files_pos dns_pos
|
||||
files_pos=$(echo "$line" | grep -bo '\bfiles\b' | head -1 | cut -d: -f1)
|
||||
dns_pos=$(echo "$line" | grep -bo '\bdns\b' | head -1 | cut -d: -f1)
|
||||
if [[ -n "$files_pos" && -n "$dns_pos" && "$files_pos" -gt "$dns_pos" ]]; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Check current nsswitch.conf hosts line
|
||||
current_hosts_line=$(grep '^hosts:' "$TARGET" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$current_hosts_line" ]]; then
|
||||
log "CRITICAL: No 'hosts:' line found in $TARGET - restoring from canonical"
|
||||
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||
chattr -i "$TARGET" 2>/dev/null || true
|
||||
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||
chmod 644 "$TARGET"
|
||||
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
|
||||
log "Restored $TARGET from canonical copy"
|
||||
else
|
||||
log "ERROR: Canonical source not found at $CANONICAL_SOURCE"
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! validate_hosts_line "$current_hosts_line"; then
|
||||
log "TAMPERING DETECTED: 'hosts:' line is invalid or missing 'files' before 'dns'"
|
||||
log "Current line: $current_hosts_line"
|
||||
|
||||
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||
chattr -i "$TARGET" 2>/dev/null || true
|
||||
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||
chmod 644 "$TARGET"
|
||||
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
|
||||
log "Restored $TARGET from canonical copy"
|
||||
else
|
||||
log "ERROR: Canonical source not found at $CANONICAL_SOURCE"
|
||||
# Emergency fix: add "files" back to hosts line
|
||||
chattr -i "$TARGET" 2>/dev/null || true
|
||||
sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$TARGET"
|
||||
chattr +i "$TARGET" 2>/dev/null || true
|
||||
log "Emergency fix applied: added 'files' before 'dns'"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If canonical exists, check for any drift
|
||||
if [[ -f "$CANONICAL_SOURCE" ]]; then
|
||||
if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then
|
||||
log "Drift detected in $TARGET (but hosts line valid) - restoring canonical"
|
||||
chattr -i "$TARGET" 2>/dev/null || true
|
||||
cp "$CANONICAL_SOURCE" "$TARGET"
|
||||
chmod 644 "$TARGET"
|
||||
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute"
|
||||
log "Restored $TARGET from canonical copy"
|
||||
else
|
||||
log "No drift detected in $TARGET"
|
||||
fi
|
||||
else
|
||||
log "Creating initial canonical snapshot"
|
||||
mkdir -p "$(dirname "$CANONICAL_SOURCE")"
|
||||
cp "$TARGET" "$CANONICAL_SOURCE"
|
||||
chmod 644 "$CANONICAL_SOURCE"
|
||||
chattr +i "$CANONICAL_SOURCE" 2>/dev/null || log "Failed to protect canonical copy"
|
||||
fi
|
||||
|
||||
# Ensure immutable attribute is set
|
||||
chattr -i "$TARGET" 2>/dev/null || true
|
||||
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
|
||||
|
||||
log "nsswitch.conf enforcement complete"
|
||||
9
hosts/guard/nsswitch-guard.path
Normal file
9
hosts/guard/nsswitch-guard.path
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Watch /etc/nsswitch.conf for tampering (hosts bypass protection)
|
||||
|
||||
[Path]
|
||||
PathChanged=/etc/nsswitch.conf
|
||||
Unit=nsswitch-guard.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
12
hosts/guard/nsswitch-guard.service
Normal file
12
hosts/guard/nsswitch-guard.service
Normal file
@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Enforce canonical /etc/nsswitch.conf (prevents hosts bypass)
|
||||
After=local-fs.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/enforce-nsswitch.sh
|
||||
Nice=10
|
||||
IOSchedulingClass=idle
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
98
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
98
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
@ -7,85 +7,85 @@ LOGTAG=hosts-guard-hook
|
||||
|
||||
# Check if target has a read-only mount
|
||||
is_ro_mount() {
|
||||
findmnt -no OPTIONS -T "$TARGET" 2> /dev/null | grep -qw ro
|
||||
findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro
|
||||
}
|
||||
|
||||
# Count mount layers for the target
|
||||
mount_layers_count() {
|
||||
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2> /dev/null || echo 0
|
||||
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
|
||||
}
|
||||
|
||||
# Collapse all bind mount layers
|
||||
collapse_mounts() {
|
||||
local i=0
|
||||
if command -v mountpoint > /dev/null 2>&1; then
|
||||
while mountpoint -q "$TARGET"; do
|
||||
umount -l "$TARGET" > /dev/null 2>&1 || break
|
||||
i=$((i + 1))
|
||||
((i > 20)) && break
|
||||
done
|
||||
else
|
||||
local cnt
|
||||
cnt=$(mount_layers_count)
|
||||
while ((cnt > 1)); do
|
||||
umount -l "$TARGET" > /dev/null 2>&1 || break
|
||||
i=$((i + 1))
|
||||
((i > 20)) && break
|
||||
cnt=$(mount_layers_count)
|
||||
done
|
||||
fi
|
||||
local i=0
|
||||
if command -v mountpoint >/dev/null 2>&1; then
|
||||
while mountpoint -q "$TARGET"; do
|
||||
umount -l "$TARGET" >/dev/null 2>&1 || break
|
||||
i=$((i + 1))
|
||||
((i > 20)) && break
|
||||
done
|
||||
else
|
||||
local cnt
|
||||
cnt=$(mount_layers_count)
|
||||
while ((cnt > 1)); do
|
||||
umount -l "$TARGET" >/dev/null 2>&1 || break
|
||||
i=$((i + 1))
|
||||
((i > 20)) && break
|
||||
cnt=$(mount_layers_count)
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Stop systemd units related to hosts guard
|
||||
stop_units_if_present() {
|
||||
local units=(hosts-bind-mount.service hosts-guard.path)
|
||||
for u in "${units[@]}"; do
|
||||
if command -v systemctl > /dev/null 2>&1; then
|
||||
if systemctl list-unit-files 2> /dev/null | grep -q "^$u"; then
|
||||
systemctl stop "$u" > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
local units=(hosts-bind-mount.service hosts-guard.path)
|
||||
for u in "${units[@]}"; do
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
|
||||
systemctl stop "$u" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Remove immutable/append-only attributes
|
||||
remove_host_attrs() {
|
||||
if command -v lsattr > /dev/null 2>&1; then
|
||||
local attrs
|
||||
attrs=$(lsattr -d "$TARGET" 2> /dev/null || true)
|
||||
if echo "$attrs" | grep -q " i "; then
|
||||
chattr -i "$TARGET" > /dev/null 2>&1 || true
|
||||
fi
|
||||
if echo "$attrs" | grep -q " a "; then
|
||||
chattr -a "$TARGET" > /dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
if command -v lsattr >/dev/null 2>&1; then
|
||||
local attrs
|
||||
attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
|
||||
if echo "$attrs" | grep -q " i "; then
|
||||
chattr -i "$TARGET" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if echo "$attrs" | grep -q " a "; then
|
||||
chattr -a "$TARGET" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply immutable attribute
|
||||
apply_immutable() {
|
||||
if command -v chattr > /dev/null 2>&1; then
|
||||
chattr +i "$TARGET" > /dev/null 2>&1 || true
|
||||
fi
|
||||
if command -v chattr >/dev/null 2>&1; then
|
||||
chattr +i "$TARGET" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Apply a single read-only bind mount layer
|
||||
apply_ro_bind_mount() {
|
||||
mount --bind "$TARGET" "$TARGET" > /dev/null 2>&1 || true
|
||||
mount -o remount,ro,bind "$TARGET" > /dev/null 2>&1 || true
|
||||
mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true
|
||||
mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# Start the path watcher service
|
||||
start_path_watcher() {
|
||||
if command -v systemctl > /dev/null 2>&1; then
|
||||
systemctl start hosts-guard.path > /dev/null 2>&1 || true
|
||||
fi
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl start hosts-guard.path >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Log to system logger and run log file
|
||||
log_hook() {
|
||||
local phase="$1"
|
||||
local state="$2"
|
||||
logger -t "$LOGTAG" "$phase: $state"
|
||||
echo "$(date -Is) $phase-$state" >> /run/hosts-guard-hook.log 2> /dev/null || true
|
||||
local phase="$1"
|
||||
local state="$2"
|
||||
logger -t "$LOGTAG" "$phase: $state"
|
||||
echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true
|
||||
}
|
||||
|
||||
2
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
2
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
@ -16,7 +16,7 @@ collapse_mounts
|
||||
|
||||
# Run enforcement script if available
|
||||
if [[ -x $ENFORCE ]]; then
|
||||
"$ENFORCE" > /dev/null 2>&1 || true
|
||||
"$ENFORCE" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Apply protections
|
||||
|
||||
3
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh
Normal file → Executable file
3
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh
Normal file → Executable file
@ -9,6 +9,7 @@ source "$SCRIPT_DIR/hosts-guard-common.sh"
|
||||
|
||||
# Remove protective attributes
|
||||
remove_host_attrs
|
||||
sudo rm /etc/hosts
|
||||
|
||||
# Stop guard services
|
||||
stop_units_if_present
|
||||
@ -20,7 +21,7 @@ collapse_mounts
|
||||
|
||||
# Ensure writable by remounting if still read-only
|
||||
if is_ro_mount; then
|
||||
mount -o remount,rw "$TARGET" > /dev/null 2>&1 || collapse_mounts
|
||||
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts
|
||||
fi
|
||||
|
||||
log_hook "pre" "unlocking(done)"
|
||||
|
||||
34
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
34
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
@ -17,30 +17,30 @@ require_root "$@"
|
||||
echo "Reason for editing /etc/hosts (will be logged):" >&2
|
||||
read -r -p "Enter reason: " REASON
|
||||
if [[ -z ${REASON// /} ]]; then
|
||||
echo "Empty reason not allowed. Aborting." >&2
|
||||
exit 1
|
||||
echo "Empty reason not allowed. Aborting." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Requested intentional /etc/hosts modification session. Reason: $REASON"
|
||||
logger -t "$SYSLOG_TAG" "session_start user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||
echo "This action is logged. A cooling-off delay of $DELAY_SECONDS seconds applies." >&2
|
||||
|
||||
for s in hosts-bind-mount.service hosts-guard.path; do
|
||||
if systemctl is-active --quiet "$s"; then
|
||||
log "Stopping $s"
|
||||
systemctl stop "$s" || true
|
||||
fi
|
||||
if systemctl is-enabled --quiet "$s"; then
|
||||
log "(Will re-enable later)"
|
||||
fi
|
||||
if systemctl is-active --quiet "$s"; then
|
||||
log "Stopping $s"
|
||||
systemctl stop "$s" || true
|
||||
fi
|
||||
if systemctl is-enabled --quiet "$s"; then
|
||||
log "(Will re-enable later)"
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove attributes to allow edit
|
||||
chattr -i -a "$TARGET" 2> /dev/null || true
|
||||
chattr -i -a "$TARGET" 2>/dev/null || true
|
||||
|
||||
echo "Countdown:" >&2
|
||||
for ((i = DELAY_SECONDS; i > 0; i--)); do
|
||||
printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2
|
||||
sleep 1
|
||||
printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2
|
||||
sleep 1
|
||||
done
|
||||
echo >&2
|
||||
|
||||
@ -50,12 +50,12 @@ sha_before=$(sha256sum "$TARGET" | awk '{print $1}')
|
||||
sha_after=$(sha256sum "$TARGET" | awk '{print $1}')
|
||||
|
||||
if [[ $sha_before == "$sha_after" ]]; then
|
||||
log "No changes made to $TARGET. Reason: $REASON"
|
||||
logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||
log "No changes made to $TARGET. Reason: $REASON"
|
||||
logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||
else
|
||||
log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON"
|
||||
logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||
cp "$TARGET" "$CANON"
|
||||
log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON"
|
||||
logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||
cp "$TARGET" "$CANON"
|
||||
fi
|
||||
|
||||
# Re-run enforcement
|
||||
|
||||
@ -33,6 +33,7 @@ FORCE_SNAPSHOT=0
|
||||
DO_SNAPSHOT=1
|
||||
ENABLE_BIND=1
|
||||
ENABLE_PATH=1
|
||||
ENABLE_NSSWITCH=1
|
||||
UNINSTALL=0
|
||||
DELAY=45
|
||||
DRY_RUN=0
|
||||
@ -48,15 +49,15 @@ note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; }
|
||||
warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; }
|
||||
err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; }
|
||||
run() {
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
printf 'DRY-RUN:'
|
||||
if [ "$#" -gt 0 ]; then
|
||||
printf ' %q' "$@"
|
||||
fi
|
||||
printf '\n'
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
printf 'DRY-RUN:'
|
||||
if [ "$#" -gt 0 ]; then
|
||||
printf ' %q' "$@"
|
||||
fi
|
||||
printf '\n'
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; }
|
||||
@ -67,73 +68,77 @@ usage() { sed -n '1,/^set -euo pipefail/p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
# Parse args
|
||||
######################################################################
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force-snapshot)
|
||||
FORCE_SNAPSHOT=1
|
||||
shift
|
||||
;;
|
||||
--no-snapshot)
|
||||
DO_SNAPSHOT=0
|
||||
shift
|
||||
;;
|
||||
--skip-bind)
|
||||
ENABLE_BIND=0
|
||||
shift
|
||||
;;
|
||||
--skip-path-watch)
|
||||
ENABLE_PATH=0
|
||||
shift
|
||||
;;
|
||||
--delay)
|
||||
DELAY=${2:-}
|
||||
[[ -z ${DELAY} ]] && {
|
||||
err '--delay requires value'
|
||||
exit 2
|
||||
}
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--no-shell-hooks)
|
||||
INSTALL_SHELL_HOOKS=0
|
||||
shift
|
||||
;;
|
||||
--shell-hooks)
|
||||
INSTALL_SHELL_HOOKS=1
|
||||
shift
|
||||
;;
|
||||
--no-audit)
|
||||
INSTALL_AUDIT_RULE=0
|
||||
shift
|
||||
;;
|
||||
--audit)
|
||||
INSTALL_AUDIT_RULE=1
|
||||
shift
|
||||
;;
|
||||
--no-alias-stub)
|
||||
ADD_ALIAS_STUB=0
|
||||
shift
|
||||
;;
|
||||
--alias-stub)
|
||||
ADD_ALIAS_STUB=1
|
||||
shift
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL=1
|
||||
shift
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
err "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
case "$1" in
|
||||
--force-snapshot)
|
||||
FORCE_SNAPSHOT=1
|
||||
shift
|
||||
;;
|
||||
--no-snapshot)
|
||||
DO_SNAPSHOT=0
|
||||
shift
|
||||
;;
|
||||
--skip-bind)
|
||||
ENABLE_BIND=0
|
||||
shift
|
||||
;;
|
||||
--skip-path-watch)
|
||||
ENABLE_PATH=0
|
||||
shift
|
||||
;;
|
||||
--skip-nsswitch)
|
||||
ENABLE_NSSWITCH=0
|
||||
shift
|
||||
;;
|
||||
--delay)
|
||||
DELAY=${2:-}
|
||||
[[ -z ${DELAY} ]] && {
|
||||
err '--delay requires value'
|
||||
exit 2
|
||||
}
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--no-shell-hooks)
|
||||
INSTALL_SHELL_HOOKS=0
|
||||
shift
|
||||
;;
|
||||
--shell-hooks)
|
||||
INSTALL_SHELL_HOOKS=1
|
||||
shift
|
||||
;;
|
||||
--no-audit)
|
||||
INSTALL_AUDIT_RULE=0
|
||||
shift
|
||||
;;
|
||||
--audit)
|
||||
INSTALL_AUDIT_RULE=1
|
||||
shift
|
||||
;;
|
||||
--no-alias-stub)
|
||||
ADD_ALIAS_STUB=0
|
||||
shift
|
||||
;;
|
||||
--alias-stub)
|
||||
ADD_ALIAS_STUB=1
|
||||
shift
|
||||
;;
|
||||
--uninstall)
|
||||
UNINSTALL=1
|
||||
shift
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
err "Unknown argument: $1"
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_root "$@"
|
||||
@ -149,11 +154,17 @@ TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh"
|
||||
UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service"
|
||||
UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path"
|
||||
UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service"
|
||||
TEMPLATE_ENFORCE_NSSWITCH="$SCRIPT_DIR/enforce-nsswitch.sh"
|
||||
UNIT_NSSWITCH_SERVICE="$SCRIPT_DIR/nsswitch-guard.service"
|
||||
UNIT_NSSWITCH_PATH="$SCRIPT_DIR/nsswitch-guard.path"
|
||||
|
||||
INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh"
|
||||
INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts"
|
||||
INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh"
|
||||
CANON="/usr/local/share/locked-hosts"
|
||||
CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf"
|
||||
HOSTS="/etc/hosts"
|
||||
NSSWITCH="/etc/nsswitch.conf"
|
||||
|
||||
# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d)
|
||||
ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh"
|
||||
@ -165,26 +176,29 @@ SYSTEMD_DIR="/etc/systemd/system"
|
||||
# Uninstall flow
|
||||
######################################################################
|
||||
if [[ $UNINSTALL -eq 1 ]]; then
|
||||
note "Uninstalling hosts guard components ( protections removed )"
|
||||
for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service; do
|
||||
if systemctl list-unit-files | grep -q "^$u"; then
|
||||
run systemctl disable --now "$u" || true
|
||||
fi
|
||||
done
|
||||
for f in \
|
||||
"$INSTALL_ENFORCE" \
|
||||
"$INSTALL_UNLOCK" \
|
||||
"$SYSTEMD_DIR/hosts-guard.service" \
|
||||
"$SYSTEMD_DIR/hosts-guard.path" \
|
||||
"$SYSTEMD_DIR/hosts-bind-mount.service" \
|
||||
"$ZSH_FILTER_SNIPPET" \
|
||||
"$BASH_FILTER_SNIPPET"; do
|
||||
if [[ -e $f ]]; then run rm -f "$f"; fi
|
||||
done
|
||||
note "Leaving canonical snapshot at $CANON (remove manually if undesired)."
|
||||
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
||||
msg "Uninstall complete"
|
||||
exit 0
|
||||
note "Uninstalling hosts guard components ( protections removed )"
|
||||
for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service; do
|
||||
if systemctl list-unit-files | grep -q "^$u"; then
|
||||
run systemctl disable --now "$u" || true
|
||||
fi
|
||||
done
|
||||
for f in \
|
||||
"$INSTALL_ENFORCE" \
|
||||
"$INSTALL_UNLOCK" \
|
||||
"$INSTALL_ENFORCE_NSSWITCH" \
|
||||
"$SYSTEMD_DIR/hosts-guard.service" \
|
||||
"$SYSTEMD_DIR/hosts-guard.path" \
|
||||
"$SYSTEMD_DIR/hosts-bind-mount.service" \
|
||||
"$SYSTEMD_DIR/nsswitch-guard.service" \
|
||||
"$SYSTEMD_DIR/nsswitch-guard.path" \
|
||||
"$ZSH_FILTER_SNIPPET" \
|
||||
"$BASH_FILTER_SNIPPET"; do
|
||||
if [[ -e $f ]]; then run rm -f "$f"; fi
|
||||
done
|
||||
note "Leaving canonical snapshots at $CANON and $CANON_NSSWITCH (remove manually if undesired)."
|
||||
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
||||
msg "Uninstall complete"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
@ -194,29 +208,29 @@ note "Script directory: $SCRIPT_DIR"
|
||||
note "Repository root: $REPO_ROOT"
|
||||
|
||||
for req in "$TEMPLATE_ENFORCE" "$TEMPLATE_UNLOCK" "$UNIT_GUARD_SERVICE"; do
|
||||
[[ -f $req ]] || {
|
||||
err "Missing template: $req"
|
||||
exit 1
|
||||
}
|
||||
[[ -f $req ]] || {
|
||||
err "Missing template: $req"
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
if [[ ! -f $HOSTS ]]; then
|
||||
err "$HOSTS does not exist. Run your hosts/install.sh first."
|
||||
exit 1
|
||||
err "$HOSTS does not exist. Run your hosts/install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
# Snapshot
|
||||
######################################################################
|
||||
if [[ $DO_SNAPSHOT -eq 1 ]]; then
|
||||
if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then
|
||||
note "Canonical snapshot exists (use --force-snapshot to overwrite)"
|
||||
else
|
||||
msg "Creating canonical snapshot at $CANON"
|
||||
run install -m 644 -D "$HOSTS" "$CANON"
|
||||
fi
|
||||
if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then
|
||||
note "Canonical snapshot exists (use --force-snapshot to overwrite)"
|
||||
else
|
||||
msg "Creating canonical snapshot at $CANON"
|
||||
run install -m 644 -D "$HOSTS" "$CANON"
|
||||
fi
|
||||
else
|
||||
note "Skipping snapshot creation (--no-snapshot)"
|
||||
note "Skipping snapshot creation (--no-snapshot)"
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
@ -230,27 +244,27 @@ run install -m 755 "$TEMPLATE_UNLOCK" "$INSTALL_UNLOCK"
|
||||
|
||||
# Adjust delay in unlock script if different from default
|
||||
if [[ $DELAY -ne 45 ]]; then
|
||||
msg "Adjusting unlock delay to $DELAY seconds"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would patch $INSTALL_UNLOCK"
|
||||
else
|
||||
# Replace DELAY_SECONDS=... line
|
||||
sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay"
|
||||
fi
|
||||
msg "Adjusting unlock delay to $DELAY seconds"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would patch $INSTALL_UNLOCK"
|
||||
else
|
||||
# Replace DELAY_SECONDS=... line
|
||||
sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay"
|
||||
fi
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
# Install shell history filters (optional)
|
||||
######################################################################
|
||||
if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then
|
||||
msg "Installing shell history suppression hooks for unlock command"
|
||||
# Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot
|
||||
# Zsh: use zshaddhistory function
|
||||
if command -v zsh > /dev/null 2>&1; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET"
|
||||
else
|
||||
cat > "$ZSH_FILTER_SNIPPET" << 'ZEOF'
|
||||
msg "Installing shell history suppression hooks for unlock command"
|
||||
# Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot
|
||||
# Zsh: use zshaddhistory function
|
||||
if command -v zsh >/dev/null 2>&1; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET"
|
||||
else
|
||||
cat >"$ZSH_FILTER_SNIPPET" <<'ZEOF'
|
||||
# Added by hosts guard setup – suppress unlock-hosts commands from Zsh history
|
||||
autoload -Uz add-zsh-hook 2>/dev/null || true
|
||||
_hosts_guard_history_filter() {
|
||||
@ -269,16 +283,16 @@ else
|
||||
zshaddhistory() { _hosts_guard_history_filter "$1"; }
|
||||
fi
|
||||
ZEOF
|
||||
chmod 644 "$ZSH_FILTER_SNIPPET"
|
||||
fi
|
||||
fi
|
||||
chmod 644 "$ZSH_FILTER_SNIPPET"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Bash: rely on HISTCONTROL and PROMPT_COMMAND filter
|
||||
if command -v bash > /dev/null 2>&1; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $BASH_FILTER_SNIPPET"
|
||||
else
|
||||
cat > "$BASH_FILTER_SNIPPET" << 'BEOF'
|
||||
# Bash: rely on HISTCONTROL and PROMPT_COMMAND filter
|
||||
if command -v bash >/dev/null 2>&1; then
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $BASH_FILTER_SNIPPET"
|
||||
else
|
||||
cat >"$BASH_FILTER_SNIPPET" <<'BEOF'
|
||||
# Added by hosts guard setup – suppress unlock-hosts commands from Bash history
|
||||
export HISTCONTROL=ignoredups:erasedups
|
||||
_hosts_guard_hist_filter() {
|
||||
@ -299,51 +313,51 @@ case :${PROMPT_COMMAND-}: in
|
||||
* ) PROMPT_COMMAND="_hosts_guard_hist_filter${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;;
|
||||
esac
|
||||
BEOF
|
||||
chmod 644 "$BASH_FILTER_SNIPPET"
|
||||
fi
|
||||
fi
|
||||
chmod 644 "$BASH_FILTER_SNIPPET"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
note "Skipping shell history hooks (--no-shell-hooks)"
|
||||
note "Skipping shell history hooks (--no-shell-hooks)"
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
# Add alias stub to discourage raw invocation (shell-level friction)
|
||||
######################################################################
|
||||
if [[ $ADD_ALIAS_STUB -eq 1 ]]; then
|
||||
PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $PROFILE_STUB"
|
||||
else
|
||||
cat > "$PROFILE_STUB" << 'ASTUB'
|
||||
PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create $PROFILE_STUB"
|
||||
else
|
||||
cat >"$PROFILE_STUB" <<'ASTUB'
|
||||
# Added by hosts guard setup – discourages casual use of unlock-hosts name
|
||||
if command -v unlock-hosts >/dev/null 2>&1; then
|
||||
alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"'
|
||||
fi
|
||||
ASTUB
|
||||
chmod 644 "$PROFILE_STUB"
|
||||
fi
|
||||
chmod 644 "$PROFILE_STUB"
|
||||
fi
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
# Audit rule to record executions (requires auditd)
|
||||
######################################################################
|
||||
if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then
|
||||
if command -v auditctl > /dev/null 2>&1; then
|
||||
audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock"
|
||||
audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock)
|
||||
if auditctl -l 2> /dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then
|
||||
note "Audit rule already present"
|
||||
else
|
||||
run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules"
|
||||
else
|
||||
echo "$audit_rule_str" > /etc/audit/rules.d/hosts_unlock.rules
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "auditctl not found; skipping audit rule (install auditd to enable)"
|
||||
fi
|
||||
if command -v auditctl >/dev/null 2>&1; then
|
||||
audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock"
|
||||
audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock)
|
||||
if auditctl -l 2>/dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then
|
||||
note "Audit rule already present"
|
||||
else
|
||||
run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules"
|
||||
else
|
||||
echo "$audit_rule_str" >/etc/audit/rules.d/hosts_unlock.rules
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "auditctl not found; skipping audit rule (install auditd to enable)"
|
||||
fi
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
@ -353,6 +367,8 @@ msg "Deploying systemd units"
|
||||
run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service"
|
||||
run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path"
|
||||
run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service"
|
||||
run install -m 644 "$UNIT_NSSWITCH_SERVICE" "$SYSTEMD_DIR/nsswitch-guard.service"
|
||||
run install -m 644 "$UNIT_NSSWITCH_PATH" "$SYSTEMD_DIR/nsswitch-guard.path"
|
||||
|
||||
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
||||
|
||||
@ -360,24 +376,51 @@ if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
||||
# Enable / Start
|
||||
######################################################################
|
||||
if [[ $ENABLE_PATH -eq 1 ]]; then
|
||||
msg "Enabling path watch (auto-revert)"
|
||||
run systemctl enable --now hosts-guard.path
|
||||
msg "Enabling path watch (auto-revert)"
|
||||
run systemctl enable --now hosts-guard.path
|
||||
else
|
||||
note "Skipping path watch (--skip-path-watch)"
|
||||
note "Skipping path watch (--skip-path-watch)"
|
||||
fi
|
||||
|
||||
if [[ $ENABLE_BIND -eq 1 ]]; then
|
||||
msg "Enabling read-only bind mount"
|
||||
run systemctl enable --now hosts-bind-mount.service
|
||||
msg "Enabling read-only bind mount"
|
||||
run systemctl enable --now hosts-bind-mount.service
|
||||
else
|
||||
note "Skipping bind mount (--skip-bind)"
|
||||
note "Skipping bind mount (--skip-bind)"
|
||||
fi
|
||||
|
||||
msg "Performing initial enforcement"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would run $INSTALL_ENFORCE"
|
||||
if [[ $ENABLE_NSSWITCH -eq 1 ]]; then
|
||||
msg "Enabling nsswitch.conf protection (hosts bypass prevention)"
|
||||
msg "Installing nsswitch enforcement script -> $INSTALL_ENFORCE_NSSWITCH"
|
||||
run install -m 755 "$TEMPLATE_ENFORCE_NSSWITCH" "$INSTALL_ENFORCE_NSSWITCH"
|
||||
|
||||
# Create nsswitch canonical snapshot if needed
|
||||
if [[ -f "$NSSWITCH" ]]; then
|
||||
if [[ ! -f "$CANON_NSSWITCH" ]]; then
|
||||
msg "Creating canonical nsswitch.conf snapshot at $CANON_NSSWITCH"
|
||||
run cp "$NSSWITCH" "$CANON_NSSWITCH"
|
||||
run chmod 644 "$CANON_NSSWITCH"
|
||||
chattr +i "$CANON_NSSWITCH" 2>/dev/null || warn "Failed to protect canonical nsswitch copy"
|
||||
fi
|
||||
fi
|
||||
|
||||
run systemctl enable --now nsswitch-guard.path
|
||||
|
||||
# Perform initial nsswitch enforcement
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would run $INSTALL_ENFORCE_NSSWITCH"
|
||||
else
|
||||
"$INSTALL_ENFORCE_NSSWITCH" || warn "nsswitch enforcement returned non-zero"
|
||||
fi
|
||||
else
|
||||
"$INSTALL_ENFORCE" || warn "Enforcement returned non-zero"
|
||||
note "Skipping nsswitch protection (--skip-nsswitch)"
|
||||
fi
|
||||
|
||||
msg "Performing initial hosts enforcement"
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
echo "DRY-RUN: would run $INSTALL_ENFORCE"
|
||||
else
|
||||
"$INSTALL_ENFORCE" || warn "Enforcement returned non-zero"
|
||||
fi
|
||||
|
||||
######################################################################
|
||||
@ -385,12 +428,15 @@ fi
|
||||
######################################################################
|
||||
echo
|
||||
msg "Hosts guard setup complete"
|
||||
echo "Canonical copy: $CANON"
|
||||
echo "Canonical hosts copy: $CANON"
|
||||
echo "Canonical nsswitch copy: $CANON_NSSWITCH"
|
||||
echo "Enforce script: $INSTALL_ENFORCE"
|
||||
echo "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH"
|
||||
echo "Unlock command: sudo $INSTALL_UNLOCK"
|
||||
echo "Delay (seconds): $DELAY"
|
||||
echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)"
|
||||
echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)"
|
||||
echo "nsswitch protection: $([[ $ENABLE_NSSWITCH -eq 1 ]] && echo enabled || echo disabled)"
|
||||
echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)"
|
||||
echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)"
|
||||
echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)"
|
||||
|
||||
427
hosts/install.sh
427
hosts/install.sh
@ -2,7 +2,7 @@
|
||||
|
||||
# Re-run with sudo if not root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo -E bash "$0" "$@"
|
||||
exec sudo -E bash "$0" "$@"
|
||||
fi
|
||||
|
||||
# Options
|
||||
@ -11,18 +11,18 @@ FLUSH_DNS=0
|
||||
|
||||
# Parse CLI flags
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--flush-dns)
|
||||
FLUSH_DNS=1
|
||||
;;
|
||||
--no-flush-dns)
|
||||
FLUSH_DNS=0
|
||||
;;
|
||||
-h | --help)
|
||||
echo "Usage: $0 [--flush-dns|--no-flush-dns]"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
case "$arg" in
|
||||
--flush-dns)
|
||||
FLUSH_DNS=1
|
||||
;;
|
||||
--no-flush-dns)
|
||||
FLUSH_DNS=0
|
||||
;;
|
||||
-h | --help)
|
||||
echo "Usage: $0 [--flush-dns|--no-flush-dns]"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ============================================================================
|
||||
@ -39,136 +39,136 @@ CUSTOM_ENTRIES_STATE_FILE="/etc/hosts.custom-entries.state"
|
||||
# Extract custom blocked entries from a hosts file or heredoc section
|
||||
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
||||
extract_custom_entries_from_script() {
|
||||
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
|
||||
local script_path="$1"
|
||||
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
|
||||
local script_path="$1"
|
||||
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
|
||||
extract_custom_entries_from_hosts() {
|
||||
local hosts_file="$1"
|
||||
if [[ ! -f $hosts_file ]]; then
|
||||
return
|
||||
fi
|
||||
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
local hosts_file="$1"
|
||||
if [[ ! -f $hosts_file ]]; then
|
||||
return
|
||||
fi
|
||||
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||
awk '{print $2}' |
|
||||
sort -u
|
||||
}
|
||||
|
||||
# Load previously saved custom entries state
|
||||
load_saved_custom_entries() {
|
||||
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
||||
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
fi
|
||||
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
||||
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# Save current custom entries to state file
|
||||
save_custom_entries_state() {
|
||||
local entries="$1"
|
||||
echo "$entries" | sort -u > "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
|
||||
local entries="$1"
|
||||
echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
||||
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Helper function to count non-empty lines
|
||||
count_lines() {
|
||||
local input="$1"
|
||||
if [[ -z $input ]]; then
|
||||
echo 0
|
||||
else
|
||||
echo "$input" | grep -c . 2> /dev/null || echo 0
|
||||
fi
|
||||
local input="$1"
|
||||
if [[ -z $input ]]; then
|
||||
echo 0
|
||||
else
|
||||
echo "$input" | grep -c . 2>/dev/null || echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main protection check
|
||||
check_custom_entries_protection() {
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
|
||||
# Get new entries from the script's heredoc
|
||||
local new_entries
|
||||
new_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
# Get new entries from the script's heredoc
|
||||
local new_entries
|
||||
new_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
local new_count
|
||||
new_count=$(count_lines "$new_entries")
|
||||
|
||||
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
||||
local saved_entries
|
||||
saved_entries=$(load_saved_custom_entries)
|
||||
if [[ -z $saved_entries ]]; then
|
||||
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
||||
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
|
||||
fi
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
||||
local saved_entries
|
||||
saved_entries=$(load_saved_custom_entries)
|
||||
if [[ -z $saved_entries ]]; then
|
||||
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
||||
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
|
||||
fi
|
||||
local saved_count
|
||||
saved_count=$(count_lines "$saved_entries")
|
||||
|
||||
# If no saved state exists, this is first installation - allow it
|
||||
if [[ $saved_count -eq 0 ]]; then
|
||||
echo "ℹ️ First installation detected - no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
# If no saved state exists, this is first installation - allow it
|
||||
if [[ $saved_count -eq 0 ]]; then
|
||||
echo "ℹ️ First installation detected - no protection check needed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find entries that were removed
|
||||
local removed_entries
|
||||
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local removed_count
|
||||
removed_count=$(count_lines "$removed_entries")
|
||||
# Find entries that were removed
|
||||
local removed_entries
|
||||
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local removed_count
|
||||
removed_count=$(count_lines "$removed_entries")
|
||||
|
||||
# Find entries that are new
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
# Find entries that are new
|
||||
local added_entries
|
||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||
local added_count
|
||||
added_count=$(count_lines "$added_entries")
|
||||
|
||||
echo ""
|
||||
echo "📊 Custom Entries Protection Check:"
|
||||
echo " Previously blocked: $saved_count entries"
|
||||
echo " Currently in script: $new_count entries"
|
||||
echo " Removed: $removed_count | Added: $added_count"
|
||||
echo ""
|
||||
echo "📊 Custom Entries Protection Check:"
|
||||
echo " Previously blocked: $saved_count entries"
|
||||
echo " Currently in script: $new_count entries"
|
||||
echo " Removed: $removed_count | Added: $added_count"
|
||||
|
||||
# RULE 1: No entries removed - always OK
|
||||
if [[ $removed_count -eq 0 ]]; then
|
||||
echo " ✅ No entries removed - protection check passed."
|
||||
return 0
|
||||
fi
|
||||
# RULE 1: No entries removed - always OK
|
||||
if [[ $removed_count -eq 0 ]]; then
|
||||
echo " ✅ No entries removed - protection check passed."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to REMOVE the following blocked entries:"
|
||||
while IFS= read -r entry; do
|
||||
echo " - $entry"
|
||||
done <<< "$removed_entries"
|
||||
echo ""
|
||||
echo "This is NOT allowed. The only way to unblock sites is to:"
|
||||
echo ""
|
||||
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
||||
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
||||
echo " (also protected with chattr)"
|
||||
echo ""
|
||||
echo "These manual steps are intentionally difficult to prevent"
|
||||
echo "impulsive unblocking. If you really need to unblock something,"
|
||||
echo "you'll have to work for it."
|
||||
echo ""
|
||||
return 1
|
||||
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "You are attempting to REMOVE the following blocked entries:"
|
||||
while IFS= read -r entry; do
|
||||
echo " - $entry"
|
||||
done <<<"$removed_entries"
|
||||
echo ""
|
||||
echo "This is NOT allowed. The only way to unblock sites is to:"
|
||||
echo ""
|
||||
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
||||
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
||||
echo " (also protected with chattr)"
|
||||
echo ""
|
||||
echo "These manual steps are intentionally difficult to prevent"
|
||||
echo "impulsive unblocking. If you really need to unblock something,"
|
||||
echo "you'll have to work for it."
|
||||
echo ""
|
||||
return 1
|
||||
}
|
||||
|
||||
# Run the protection check
|
||||
if ! check_custom_entries_protection; then
|
||||
exit 1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Enable systemd-resolved
|
||||
sudo systemctl enable systemd-resolved
|
||||
|
||||
# Remove all attributes from /etc/hosts to allow modifications
|
||||
sudo chattr -i -a /etc/hosts 2> /dev/null || true
|
||||
sudo chattr -i -a /etc/hosts 2>/dev/null || true
|
||||
|
||||
# Source and local cache configuration
|
||||
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
|
||||
@ -177,33 +177,33 @@ LOCAL_CACHE="/etc/hosts.stevenblack"
|
||||
|
||||
# Helpers
|
||||
extract_date_epoch_from_file() {
|
||||
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
||||
local f="$1"
|
||||
local line
|
||||
line=$(grep -m1 '^# Date:' "$f" 2> /dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
||||
if [[ -n $line ]]; then
|
||||
date -u -d "$line" +%s 2> /dev/null || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
||||
local f="$1"
|
||||
local line
|
||||
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
||||
if [[ -n $line ]]; then
|
||||
date -u -d "$line" +%s 2>/dev/null || echo ""
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_remote_header() {
|
||||
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
||||
local out="$1"
|
||||
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
||||
return 0
|
||||
fi
|
||||
# Fallback – may download more, but we only keep first lines
|
||||
if curl -LfsS --max-time 10 "$URL" | head -n 20 > "$out"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
||||
local out="$1"
|
||||
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
||||
return 0
|
||||
fi
|
||||
# Fallback – may download more, but we only keep first lines
|
||||
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
download_remote_full_to() {
|
||||
local out="$1"
|
||||
curl -LfsS "$URL" -o "$out"
|
||||
local out="$1"
|
||||
curl -LfsS "$URL" -o "$out"
|
||||
}
|
||||
|
||||
# Decide whether to use cache or update
|
||||
@ -212,47 +212,47 @@ trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
|
||||
|
||||
REMOTE_AVAILABLE=0
|
||||
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
|
||||
REMOTE_AVAILABLE=1
|
||||
REMOTE_AVAILABLE=1
|
||||
fi
|
||||
|
||||
NEED_UPDATE=0
|
||||
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
||||
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
||||
else
|
||||
local_epoch=""
|
||||
local_epoch=""
|
||||
fi
|
||||
|
||||
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
|
||||
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
||||
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
||||
echo "Using cached StevenBlack hosts (up-to-date)."
|
||||
else
|
||||
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
||||
NEED_UPDATE=1
|
||||
fi
|
||||
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
||||
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
||||
echo "Using cached StevenBlack hosts (up-to-date)."
|
||||
else
|
||||
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
||||
NEED_UPDATE=1
|
||||
fi
|
||||
else
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
echo "No internet; using cached StevenBlack hosts."
|
||||
else
|
||||
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ -f $LOCAL_CACHE ]]; then
|
||||
echo "No internet; using cached StevenBlack hosts."
|
||||
else
|
||||
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Ensure we have a fresh cache if needed
|
||||
if [[ $NEED_UPDATE -eq 1 ]]; then
|
||||
TMP_DL=$(mktemp)
|
||||
if download_remote_full_to "$TMP_DL"; then
|
||||
# Save raw upstream to cache
|
||||
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
||||
sudo chmod 644 "$LOCAL_CACHE"
|
||||
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
||||
else
|
||||
rm -f "$TMP_DL"
|
||||
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
||||
exit 1
|
||||
fi
|
||||
TMP_DL=$(mktemp)
|
||||
if download_remote_full_to "$TMP_DL"; then
|
||||
# Save raw upstream to cache
|
||||
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
||||
sudo chmod 644 "$LOCAL_CACHE"
|
||||
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
||||
else
|
||||
rm -f "$TMP_DL"
|
||||
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install the base hosts from cache into /etc/hosts
|
||||
@ -272,7 +272,7 @@ sudo sed -i 's/^0\.0\.0\.0 messenger\.com/#0.0.0.0 messenger.com/' /etc/hosts
|
||||
|
||||
# Add custom entries for YouTube and Discord
|
||||
echo "Adding custom entries for YouTube and Discord..."
|
||||
tee -a /etc/hosts > /dev/null << 'EOF'
|
||||
tee -a /etc/hosts >/dev/null <<'EOF'
|
||||
|
||||
# Custom blocking entries
|
||||
# YouTube
|
||||
@ -295,15 +295,15 @@ tee -a /etc/hosts > /dev/null << 'EOF'
|
||||
|
||||
# Steam Store
|
||||
|
||||
# Discord (selective blocking - media only, voice chat allowed)
|
||||
0.0.0.0 cdn.discordapp.com
|
||||
0.0.0.0 media.discordapp.net
|
||||
0.0.0.0 images-ext-1.discordapp.net
|
||||
0.0.0.0 images-ext-2.discordapp.net
|
||||
0.0.0.0 attachments-1.discordapp.net
|
||||
0.0.0.0 attachments-2.discordapp.net
|
||||
0.0.0.0 tenor.com
|
||||
0.0.0.0 giphy.com
|
||||
# Discord - media allowed
|
||||
# 0.0.0.0 cdn.discordapp.com
|
||||
# 0.0.0.0 media.discordapp.net
|
||||
# 0.0.0.0 images-ext-1.discordapp.net
|
||||
# 0.0.0.0 images-ext-2.discordapp.net
|
||||
# 0.0.0.0 attachments-1.discordapp.net
|
||||
# 0.0.0.0 attachments-2.discordapp.net
|
||||
# 0.0.0.0 tenor.com
|
||||
# 0.0.0.0 giphy.com
|
||||
|
||||
# Food Delivery Services
|
||||
# Polish services
|
||||
@ -407,20 +407,111 @@ echo "Saving custom entries state for protection mechanism..."
|
||||
script_path="$(readlink -f "$0")"
|
||||
current_custom_entries=$(extract_custom_entries_from_script "$script_path")
|
||||
# Remove immutable from state file if it exists
|
||||
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
|
||||
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||
save_custom_entries_state "$current_custom_entries"
|
||||
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
||||
|
||||
# Optionally flush DNS caches
|
||||
if [[ $FLUSH_DNS -eq 1 ]]; then
|
||||
echo "Flushing DNS caches..."
|
||||
sudo systemd-resolve --flush-caches
|
||||
sudo systemctl restart NetworkManager.service
|
||||
echo "Flushing DNS caches..."
|
||||
sudo systemd-resolve --flush-caches
|
||||
sudo systemctl restart NetworkManager.service
|
||||
else
|
||||
echo "DNS cache flush skipped (use --flush-dns to enable)."
|
||||
echo "DNS cache flush skipped (use --flush-dns to enable)."
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# DISABLE DNS OVER HTTPS (DoH) IN BROWSERS
|
||||
# ============================================================================
|
||||
# DoH bypasses /etc/hosts entirely, defeating all our blocking!
|
||||
# We disable it in Firefox profiles for all users.
|
||||
echo ""
|
||||
echo "Disabling DNS over HTTPS (DoH) in browsers..."
|
||||
|
||||
# Get the actual user (not root) who invoked this script
|
||||
REAL_USER="${SUDO_USER:-$USER}"
|
||||
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
|
||||
|
||||
# Firefox: disable DoH via user.js
|
||||
if [[ -d "$REAL_HOME/.mozilla/firefox" ]]; then
|
||||
for profile in "$REAL_HOME/.mozilla/firefox"/*.default*; do
|
||||
if [[ -d "$profile" ]]; then
|
||||
cat >>"$profile/user.js" <<'FIREFOXEOF'
|
||||
// Disable DNS over HTTPS (DoH) to ensure /etc/hosts blocking works
|
||||
// Added by linux-configuration hosts installer
|
||||
user_pref("network.trr.mode", 5); // 5 = Off by user choice
|
||||
user_pref("doh-rollout.enabled", false);
|
||||
user_pref("doh-rollout.disable-heuristics", true);
|
||||
FIREFOXEOF
|
||||
chown "$REAL_USER:$REAL_USER" "$profile/user.js"
|
||||
echo " Firefox DoH disabled in: $(basename "$profile")"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo " No Firefox profiles found"
|
||||
fi
|
||||
|
||||
# Chromium-based browsers: use policy file
|
||||
CHROME_POLICY_DIR="/etc/chromium/policies/managed"
|
||||
if [[ -d "/etc/chromium" ]] || command -v chromium &>/dev/null; then
|
||||
mkdir -p "$CHROME_POLICY_DIR"
|
||||
cat >"$CHROME_POLICY_DIR/disable-doh.json" <<'CHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
CHROMEEOF
|
||||
echo " Chromium DoH disabled via policy"
|
||||
fi
|
||||
|
||||
# Google Chrome policy
|
||||
GCHROME_POLICY_DIR="/etc/opt/chrome/policies/managed"
|
||||
if [[ -d "/etc/opt/chrome" ]] || command -v google-chrome &>/dev/null; then
|
||||
mkdir -p "$GCHROME_POLICY_DIR"
|
||||
cat >"$GCHROME_POLICY_DIR/disable-doh.json" <<'GCHROMEEOF'
|
||||
{
|
||||
"DnsOverHttpsMode": "off",
|
||||
"BuiltInDnsClientEnabled": false
|
||||
}
|
||||
GCHROMEEOF
|
||||
echo " Google Chrome DoH disabled via policy"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Installation complete!"
|
||||
echo " Custom entries protection is now active."
|
||||
echo " Removing blocked entries from the script will be blocked."
|
||||
echo " DNS over HTTPS (DoH) has been disabled in browsers."
|
||||
|
||||
# ============================================================================
|
||||
# FORCE BROWSER RESTART TO APPLY DOH CHANGES
|
||||
# ============================================================================
|
||||
# Kill all browser processes so DoH changes take effect immediately
|
||||
echo ""
|
||||
echo "Killing browsers to apply DoH policy changes..."
|
||||
BROWSERS_KILLED=0
|
||||
|
||||
for browser in chrome chromium chromium-browser brave brave-browser firefox firefox-esr thorium vivaldi opera; do
|
||||
if pgrep -x "$browser" &>/dev/null || pgrep -f "/opt/.*/$browser" &>/dev/null; then
|
||||
echo " Killing $browser..."
|
||||
pkill -9 -f "$browser" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Also kill by common binary paths
|
||||
for pattern in "/opt/google/chrome" "/opt/brave" "/opt/thorium" "/usr/lib/firefox" "/usr/lib/chromium"; do
|
||||
if pgrep -f "$pattern" &>/dev/null; then
|
||||
echo " Killing processes matching $pattern..."
|
||||
pkill -9 -f "$pattern" 2>/dev/null || true
|
||||
BROWSERS_KILLED=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $BROWSERS_KILLED -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "⚠️ Browsers were killed to apply DNS settings."
|
||||
echo " Reopen your browser - hosts blocking is now enforced."
|
||||
else
|
||||
echo " No browsers were running."
|
||||
fi
|
||||
|
||||
234
scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md
Normal file
234
scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md
Normal file
@ -0,0 +1,234 @@
|
||||
# Block Compulsive Opening - LLM Reference Guide
|
||||
|
||||
> **For AI assistants**: This document explains the compulsive opening blocker so you can make correct modifications.
|
||||
|
||||
## System Purpose
|
||||
|
||||
Limit messaging apps (Beeper, Signal, Discord) to **one launch per hour** to reduce compulsive checking behavior.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ LAUNCH INTERCEPTION │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User clicks "Discord" in app launcher │
|
||||
│ ↓ │
|
||||
│ /usr/bin/discord (wrapper script) │
|
||||
│ ↓ │
|
||||
│ exec /usr/local/bin/block-compulsive-opening.sh wrapper discord │
|
||||
│ ↓ │
|
||||
│ Check: ~/.local/state/compulsive-block/discord.lastopen │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────┴─────────────────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ Not opened this hour ▼ Already opened │
|
||||
│ Record opening time Show notification │
|
||||
│ Launch real binary Exit with error │
|
||||
│ /opt/discord/Discord │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/usr/local/bin/block-compulsive-opening.sh` | Installed main script |
|
||||
| `/usr/bin/beeper` | Wrapper (replaces original) |
|
||||
| `/usr/bin/signal-desktop` | Wrapper (replaces original) |
|
||||
| `/usr/bin/discord` | Wrapper (replaces original) |
|
||||
| `/usr/bin/*.orig` or `SYMLINK:*` | Original binaries/links |
|
||||
| `~/.local/state/compulsive-block/*.lastopen` | Per-app hour tracking |
|
||||
| `~/.local/state/compulsive-block/compulsive-block.log` | Activity log |
|
||||
| `/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook` | Auto-rewrap hook |
|
||||
|
||||
## Managed Applications
|
||||
|
||||
```bash
|
||||
declare -A APPS=(
|
||||
["beeper"]="/usr/bin/beeper"
|
||||
["signal-desktop"]="/usr/bin/signal-desktop"
|
||||
["discord"]="/usr/bin/discord"
|
||||
)
|
||||
|
||||
declare -A REAL_BINARIES=(
|
||||
["beeper"]="/opt/beeper/beepertexts"
|
||||
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
|
||||
["discord"]="/opt/discord/Discord"
|
||||
)
|
||||
```
|
||||
|
||||
## State Tracking
|
||||
|
||||
Hour key format: `YYYY-MM-DD-HH` (e.g., `2026-02-02-14`)
|
||||
|
||||
State file content: Just the hour key string
|
||||
|
||||
```bash
|
||||
# Check if opened this hour
|
||||
cat ~/.local/state/compulsive-block/discord.lastopen
|
||||
# Output: 2026-02-02-14
|
||||
|
||||
# Current hour
|
||||
date '+%Y-%m-%d-%H'
|
||||
# Output: 2026-02-02-15 (different = can open again)
|
||||
```
|
||||
|
||||
## Wrapper Installation Process
|
||||
|
||||
When `install_all()` runs:
|
||||
|
||||
1. Copies script to `/usr/local/bin/block-compulsive-opening.sh`
|
||||
2. For each app:
|
||||
- If original is a symlink: Save `SYMLINK:/target/path` to `.orig`
|
||||
- If original is a file: Move to `.orig`
|
||||
- Create wrapper script at original location:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
exec /usr/local/bin/block-compulsive-opening.sh wrapper "discord" "$@"
|
||||
```
|
||||
3. Install pacman hook for auto-rewrap
|
||||
|
||||
## Pacman Hook
|
||||
|
||||
After beeper/signal/discord package updates, the hook re-wraps them:
|
||||
|
||||
```ini
|
||||
[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Install
|
||||
Type = Package
|
||||
Target = beeper
|
||||
Target = signal-desktop
|
||||
Target = discord
|
||||
|
||||
[Action]
|
||||
When = PostTransaction
|
||||
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
||||
```
|
||||
|
||||
The `rewrap-quiet` command:
|
||||
- Checks if wrapper was overwritten (doesn't contain "block-compulsive-opening")
|
||||
- If overwritten: removes stale `.orig`, re-installs wrapper
|
||||
- Logs to activity log
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Install all wrappers (requires root)
|
||||
sudo ./block_compulsive_opening.sh install
|
||||
|
||||
# Uninstall all wrappers (requires root)
|
||||
sudo ./block_compulsive_opening.sh uninstall
|
||||
|
||||
# Check status of all apps
|
||||
./block_compulsive_opening.sh status
|
||||
|
||||
# Reset a specific app (allow opening again this hour)
|
||||
./block_compulsive_opening.sh reset discord
|
||||
|
||||
# Reset all apps
|
||||
./block_compulsive_opening.sh reset-all
|
||||
```
|
||||
|
||||
## Log Format
|
||||
|
||||
```
|
||||
2026-02-02 14:30:15 - ALLOWED: discord opened (first time this hour: 2026-02-02-14)
|
||||
2026-02-02 14:30:15 - LAUNCHED: discord with PID 12345 (auto-close in 10m)
|
||||
2026-02-02 14:38:15 - (notification: "Session will end in 2 minutes")
|
||||
2026-02-02 14:40:15 - AUTO-CLOSED: discord (PID 12345) after 10m
|
||||
2026-02-02 14:45:22 - BLOCKED: discord launch prevented (already opened this hour: 2026-02-02-14)
|
||||
2026-02-02 15:01:03 - ALLOWED: discord opened (first time this hour: 2026-02-02-15)
|
||||
2026-02-02 15:30:00 - RESET: discord state cleared by user
|
||||
```
|
||||
|
||||
## Auto-Close Timer (Session Limit)
|
||||
|
||||
Apps are automatically closed after **10 minutes** to prevent indefinite usage:
|
||||
|
||||
1. When app launches, a background daemon is spawned
|
||||
2. At **8 minutes**: Warning notification "Session will end in 2 minutes"
|
||||
3. At **10 minutes**: App is closed with SIGTERM, then SIGKILL if needed
|
||||
4. State file `~/.local/state/compulsive-block/<app>.running` tracks PID and start time
|
||||
|
||||
**Configuration variables** (in script):
|
||||
```bash
|
||||
AUTO_CLOSE_TIMEOUT_MINUTES=10 # Total session length
|
||||
AUTO_CLOSE_WARNING_MINUTES=2 # Warning before close
|
||||
```
|
||||
|
||||
## Adding a New App
|
||||
|
||||
1. Add to `APPS` associative array:
|
||||
```bash
|
||||
declare -A APPS=(
|
||||
# ... existing apps ...
|
||||
["newapp"]="/usr/bin/newapp"
|
||||
)
|
||||
```
|
||||
|
||||
2. Add to `REAL_BINARIES`:
|
||||
```bash
|
||||
declare -A REAL_BINARIES=(
|
||||
# ... existing apps ...
|
||||
["newapp"]="/opt/newapp/actual-binary"
|
||||
)
|
||||
```
|
||||
|
||||
3. Add to pacman hook targets (if installed via pacman):
|
||||
```ini
|
||||
Target = newapp
|
||||
```
|
||||
|
||||
4. Reinstall:
|
||||
```bash
|
||||
sudo ./block_compulsive_opening.sh install
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Check if wrapper is installed
|
||||
```bash
|
||||
cat /usr/bin/discord
|
||||
# Should show wrapper script, not binary
|
||||
|
||||
ls -la /usr/bin/discord.orig
|
||||
# Should exist (or check for SYMLINK: content)
|
||||
```
|
||||
|
||||
### Check current state
|
||||
```bash
|
||||
./block_compulsive_opening.sh status
|
||||
# Shows: which apps are wrapped, last open times, current hour
|
||||
```
|
||||
|
||||
### Test manually
|
||||
```bash
|
||||
# Simulate wrapper call
|
||||
/usr/local/bin/block-compulsive-opening.sh wrapper discord
|
||||
```
|
||||
|
||||
### View logs
|
||||
```bash
|
||||
tail -f ~/.local/state/compulsive-block/compulsive-block.log
|
||||
```
|
||||
|
||||
## Notification Behavior
|
||||
|
||||
When blocked, shows desktop notification:
|
||||
- Title: "🚫 discord Blocked"
|
||||
- Message: "Already opened this hour. Wait until the next hour."
|
||||
- Urgency: critical
|
||||
- Timeout: 5000ms
|
||||
|
||||
Uses `notify-send` (falls back silently if not available).
|
||||
|
||||
## DO NOT
|
||||
|
||||
1. ❌ Delete `.orig` files (cannot restore original binaries)
|
||||
2. ❌ Manually edit wrapper scripts at `/usr/bin/` (will be overwritten)
|
||||
3. ❌ Assume app is "blocked" once notification shows (it ran, just not again)
|
||||
4. ❌ Remove pacman hook without understanding auto-rewrap won't work
|
||||
277
scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md
Normal file
277
scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md
Normal file
@ -0,0 +1,277 @@
|
||||
# Midnight Shutdown System - LLM Reference Guide
|
||||
|
||||
> **For AI assistants**: This document explains the automatic shutdown system so you can make correct modifications.
|
||||
|
||||
## System Purpose
|
||||
|
||||
Automatically shut down the PC during configured time windows to enforce healthy sleep schedules:
|
||||
- **Monday-Wednesday**: Shutdown at 24:00 (midnight)
|
||||
- **Thursday-Sunday**: Shutdown at 24:00 (midnight)
|
||||
- **Morning**: Safe time starts at 00:00 (effectively no morning block)
|
||||
|
||||
The times above are defaults; actual values in `/etc/shutdown-schedule.conf`.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ SHUTDOWN SYSTEM LAYERS │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Layer 1: Systemd Timer │
|
||||
│ ───────────────────── │
|
||||
│ day-specific-shutdown.timer fires every minute │
|
||||
│ day-specific-shutdown.service runs the check script │
|
||||
│ │
|
||||
│ Layer 2: Check Script │
|
||||
│ ──────────────────── │
|
||||
│ /usr/local/bin/day-specific-shutdown-check.sh │
|
||||
│ Reads config, checks current time, initiates shutdown if in window │
|
||||
│ │
|
||||
│ Layer 3: Config Protection │
|
||||
│ ──────────────────────── │
|
||||
│ /etc/shutdown-schedule.conf has chattr +i │
|
||||
│ Canonical copy at /usr/local/share/locked-shutdown-schedule.conf │
|
||||
│ Path watcher auto-restores if tampered │
|
||||
│ │
|
||||
│ Layer 4: Timer Monitor │
|
||||
│ ───────────────────── │
|
||||
│ shutdown-timer-monitor.service watches timer status │
|
||||
│ Re-enables timer if user tries to disable it │
|
||||
│ │
|
||||
│ Layer 5: Script Protection │
|
||||
│ ──────────────────────── │
|
||||
│ Setup script blocks making schedule MORE LENIENT │
|
||||
│ Can only make it STRICTER without the unlock script │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose | Protection |
|
||||
|------|---------|------------|
|
||||
| `/etc/shutdown-schedule.conf` | Runtime config | chattr +i, path watcher |
|
||||
| `/usr/local/share/locked-shutdown-schedule.conf` | Canonical copy | chattr +i |
|
||||
| `/usr/local/bin/day-specific-shutdown-check.sh` | Shutdown logic | None |
|
||||
| `/usr/local/bin/day-specific-shutdown-manager.sh` | Status/management | None |
|
||||
| `/usr/local/bin/shutdown-timer-monitor.sh` | Timer re-enabler | None |
|
||||
| `/usr/local/sbin/enforce-shutdown-schedule.sh` | Config restoration | None |
|
||||
| `/usr/local/sbin/unlock-shutdown-schedule` | Delayed config edit | None |
|
||||
| `/etc/systemd/system/day-specific-shutdown.timer` | Timer unit | systemd |
|
||||
| `/etc/systemd/system/day-specific-shutdown.service` | Service unit | systemd |
|
||||
| `/etc/systemd/system/shutdown-schedule-guard.path` | Config watcher | systemd |
|
||||
| `/etc/systemd/system/shutdown-schedule-guard.service` | Enforcement | systemd |
|
||||
| `/etc/systemd/system/shutdown-timer-monitor.service` | Timer guardian | systemd |
|
||||
| `/var/log/shutdown-schedule-guard.log` | Tampering log | None |
|
||||
|
||||
## Config File Format
|
||||
|
||||
```bash
|
||||
# /etc/shutdown-schedule.conf
|
||||
|
||||
# Shutdown hour for Monday-Wednesday (24-hour format)
|
||||
MON_WED_HOUR=21
|
||||
|
||||
# Shutdown hour for Thursday-Sunday (24-hour format)
|
||||
THU_SUN_HOUR=22
|
||||
|
||||
# Morning end hour (shutdown window ends at this hour)
|
||||
MORNING_END_HOUR=5
|
||||
```
|
||||
|
||||
**Interpretation**:
|
||||
- Mon-Wed: Shutdown if current hour >= 21 OR current hour < 5
|
||||
- Thu-Sun: Shutdown if current hour >= 22 OR current hour < 5
|
||||
|
||||
## Schedule Protection Logic
|
||||
|
||||
The setup script (`setup_midnight_shutdown.sh`) has constants at the top:
|
||||
```bash
|
||||
SCHEDULE_MON_WED_HOUR=24
|
||||
SCHEDULE_THU_SUN_HOUR=24
|
||||
SCHEDULE_MORNING_END_HOUR=0
|
||||
```
|
||||
|
||||
When re-run, it compares these to the canonical config:
|
||||
|
||||
| Change Type | Action |
|
||||
|-------------|--------|
|
||||
| Making shutdown EARLIER | ✅ Allowed without unlock |
|
||||
| Making shutdown LATER | ❌ Blocked, requires unlock |
|
||||
| Making morning end EARLIER | ❌ Always blocked |
|
||||
| Making morning end LATER | ✅ Allowed (extends shutdown window) |
|
||||
|
||||
Example blocked attempt:
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════════════╗
|
||||
║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║
|
||||
╚══════════════════════════════════════════════════════════════════╝
|
||||
|
||||
You modified the script to make the shutdown schedule MORE LENIENT:
|
||||
• Mon-Wed shutdown: 21:00 → 23:00 (later)
|
||||
|
||||
Nice try! But this is exactly the kind of late-night bargaining
|
||||
that this protection is designed to prevent. 😉
|
||||
```
|
||||
|
||||
## Unlock Script Behavior
|
||||
|
||||
`/usr/local/sbin/unlock-shutdown-schedule`:
|
||||
|
||||
1. Stops `shutdown-schedule-guard.path`
|
||||
2. Removes chattr from both config files
|
||||
3. Opens editor on temp copy
|
||||
4. Checks what changed:
|
||||
- **Stricter (earlier)**: No delay, applies immediately
|
||||
- **Lenient (later)**: 45-second countdown, then applies
|
||||
- **Lower morning end**: **ALWAYS BLOCKED** (cannot shorten window)
|
||||
5. Updates both config and canonical
|
||||
6. Re-applies chattr +i
|
||||
7. Restarts path watcher
|
||||
|
||||
## Integration Points
|
||||
|
||||
### i3blocks Countdown
|
||||
`i3blocks/shutdown_countdown.sh` reads the config to show time remaining:
|
||||
```bash
|
||||
source /etc/shutdown-schedule.conf
|
||||
# Calculates and displays "Shutdown in X:XX"
|
||||
```
|
||||
|
||||
### Screen Locker
|
||||
`screen_lock.py` can adjust shutdown time:
|
||||
- **Sick day**: Moves shutdown 1.5 hours EARLIER (penalty)
|
||||
- **Workout completed**: Moves shutdown 1.5 hours LATER (reward)
|
||||
|
||||
Uses `adjust_shutdown_schedule.sh` helper script.
|
||||
|
||||
## Systemd Units
|
||||
|
||||
### Timer (fires every minute)
|
||||
```ini
|
||||
[Timer]
|
||||
OnCalendar=*:*:00
|
||||
Persistent=false
|
||||
AccuracySec=1s
|
||||
```
|
||||
|
||||
### Check Service
|
||||
```ini
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/day-specific-shutdown-check.sh
|
||||
```
|
||||
|
||||
### Path Watcher
|
||||
```ini
|
||||
[Path]
|
||||
PathChanged=/etc/shutdown-schedule.conf
|
||||
Unit=shutdown-schedule-guard.service
|
||||
```
|
||||
|
||||
## Check Script Logic
|
||||
|
||||
```bash
|
||||
# Pseudocode for day-specific-shutdown-check.sh
|
||||
|
||||
source /etc/shutdown-schedule.conf
|
||||
day=$(date +%u) # 1=Monday, 7=Sunday
|
||||
hour=$(date +%H)
|
||||
|
||||
if [[ $day -le 3 ]]; then
|
||||
shutdown_hour=$MON_WED_HOUR
|
||||
else
|
||||
shutdown_hour=$THU_SUN_HOUR
|
||||
fi
|
||||
|
||||
# Check if in shutdown window
|
||||
if [[ $hour -ge $shutdown_hour ]] || [[ $hour -lt $MORNING_END_HOUR ]]; then
|
||||
systemctl poweroff
|
||||
fi
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Check Current Status
|
||||
```bash
|
||||
/usr/local/bin/day-specific-shutdown-manager.sh status
|
||||
# Or run setup script with 'status' argument
|
||||
```
|
||||
|
||||
### Make Schedule Stricter
|
||||
Edit the constants in `setup_midnight_shutdown.sh`:
|
||||
```bash
|
||||
SCHEDULE_MON_WED_HOUR=20 # Changed from 21 to 20 (earlier)
|
||||
```
|
||||
Then re-run:
|
||||
```bash
|
||||
sudo ./setup_midnight_shutdown.sh
|
||||
```
|
||||
|
||||
### Make Schedule More Lenient (Requires Unlock)
|
||||
```bash
|
||||
sudo /usr/local/sbin/unlock-shutdown-schedule
|
||||
# Wait for delay, edit config, save
|
||||
```
|
||||
|
||||
### Disable Timer (Will Be Re-Enabled!)
|
||||
```bash
|
||||
sudo systemctl disable --now day-specific-shutdown.timer
|
||||
# Monitor service will re-enable it automatically
|
||||
```
|
||||
|
||||
### Check Protection Status
|
||||
```bash
|
||||
lsattr /etc/shutdown-schedule.conf
|
||||
# Should show: ----i--------e--
|
||||
|
||||
systemctl status shutdown-schedule-guard.path
|
||||
systemctl status shutdown-timer-monitor.service
|
||||
```
|
||||
|
||||
## KNOWN VULNERABILITIES
|
||||
|
||||
1. **Information Disclosure**: Error messages tell user exactly how to bypass
|
||||
2. **Unlock Script Discoverable**: Path mentioned in error messages
|
||||
3. **Timer Monitor Killable**: User can stop the monitor then the timer
|
||||
4. **Check Script Unprotected**: `/usr/local/bin/day-specific-shutdown-check.sh` can be edited
|
||||
|
||||
**TODO**:
|
||||
- Remove helpful bypass instructions from error messages
|
||||
- Rename unlock script to obscure name
|
||||
- Protect check script with integrity verification
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Timer not firing
|
||||
```bash
|
||||
systemctl status day-specific-shutdown.timer
|
||||
systemctl list-timers | grep shutdown
|
||||
```
|
||||
|
||||
### Config not being enforced
|
||||
```bash
|
||||
# Check path watcher
|
||||
systemctl status shutdown-schedule-guard.path
|
||||
|
||||
# Manually trigger enforcement
|
||||
sudo /usr/local/sbin/enforce-shutdown-schedule.sh
|
||||
```
|
||||
|
||||
### Wrong time shown in i3blocks
|
||||
```bash
|
||||
# Verify config
|
||||
cat /etc/shutdown-schedule.conf
|
||||
|
||||
# Check i3blocks config
|
||||
cat ~/.config/i3blocks/config | grep shutdown
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
1. ❌ Edit setup script constants to make schedule later (will be blocked)
|
||||
2. ❌ Delete canonical config (breaks restoration)
|
||||
3. ❌ Stop `shutdown-timer-monitor.service` (timer will be re-enabled anyway)
|
||||
4. ❌ Modify check script to skip shutdown (defeats purpose)
|
||||
5. ❌ Lower `MORNING_END_HOUR` (always blocked, shortens shutdown window)
|
||||
@ -12,182 +12,298 @@ set -euo pipefail
|
||||
# Send desktop notification (inlined from common.sh to avoid dependency issues
|
||||
# when script is installed to /usr/local/bin)
|
||||
notify() {
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local urgency="${3:-normal}"
|
||||
local timeout="${4:-5000}"
|
||||
local title="$1"
|
||||
local message="$2"
|
||||
local urgency="${3:-normal}"
|
||||
local timeout="${4:-5000}"
|
||||
|
||||
if command -v notify-send &> /dev/null; then
|
||||
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true
|
||||
fi
|
||||
if command -v notify-send &>/dev/null; then
|
||||
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Configuration
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block"
|
||||
LOG_FILE="$STATE_DIR/compulsive-block.log"
|
||||
|
||||
# Auto-close timeout in minutes (apps forcefully closed after this)
|
||||
AUTO_CLOSE_TIMEOUT_MINUTES=10
|
||||
# Warning before auto-close (in minutes before timeout)
|
||||
AUTO_CLOSE_WARNING_MINUTES=2
|
||||
|
||||
# Apps to limit (name -> binary path)
|
||||
# These are the primary wrapper locations (what the user calls)
|
||||
declare -A APPS=(
|
||||
["beeper"]="/usr/bin/beeper"
|
||||
["signal-desktop"]="/usr/bin/signal-desktop"
|
||||
["discord"]="/usr/bin/discord"
|
||||
["beeper"]="/usr/bin/beeper"
|
||||
["signal-desktop"]="/usr/bin/signal-desktop"
|
||||
["discord"]="/usr/bin/discord"
|
||||
)
|
||||
|
||||
# Actual executable paths (the real binaries to exec after wrapper check)
|
||||
# These are where the real code lives
|
||||
declare -A REAL_BINARIES=(
|
||||
["beeper"]="/opt/beeper/beepertexts"
|
||||
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
|
||||
["discord"]="/opt/discord/Discord"
|
||||
["beeper"]="/opt/beeper/beepertexts"
|
||||
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
|
||||
["discord"]="/opt/discord/Discord"
|
||||
)
|
||||
|
||||
# Ensure state directory exists
|
||||
ensure_state_dir() {
|
||||
mkdir -p "$STATE_DIR" 2> /dev/null || true
|
||||
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Log message with timestamp
|
||||
log_message() {
|
||||
local msg
|
||||
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
echo "$msg" >&2
|
||||
echo "$msg" >> "$LOG_FILE" 2> /dev/null || true
|
||||
local msg
|
||||
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
echo "$msg" >&2
|
||||
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get current hour key (YYYY-MM-DD-HH format)
|
||||
get_hour_key() {
|
||||
date '+%Y-%m-%d-%H'
|
||||
date '+%Y-%m-%d-%H'
|
||||
}
|
||||
|
||||
# Get state file path for an app
|
||||
get_state_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.lastopen"
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.lastopen"
|
||||
}
|
||||
|
||||
# Check if app was already opened this hour
|
||||
was_opened_this_hour() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
return 0 # Was opened this hour
|
||||
fi
|
||||
fi
|
||||
return 1 # Not opened this hour
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
return 0 # Was opened this hour
|
||||
fi
|
||||
fi
|
||||
return 1 # Not opened this hour
|
||||
}
|
||||
|
||||
# Record app opening
|
||||
record_opening() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "$current_hour" > "$state_file"
|
||||
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
||||
echo "$current_hour" >"$state_file"
|
||||
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
||||
}
|
||||
|
||||
# Block app and notify
|
||||
block_app() {
|
||||
local app="$1"
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
local app="$1"
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
||||
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
||||
|
||||
# Send notification using common library
|
||||
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
||||
# Send notification using common library
|
||||
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
||||
}
|
||||
|
||||
# Get real binary path for an app
|
||||
get_real_binary() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if wrapper is installed (original moved to .orig)
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
# Wrapper installed, return the actual executable
|
||||
echo "$real_binary"
|
||||
return 0
|
||||
fi
|
||||
# Check if wrapper is installed (original moved to .orig)
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
# Wrapper installed, return the actual executable
|
||||
echo "$real_binary"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
return 1
|
||||
}
|
||||
|
||||
# Get running state file path for an app (tracks PID and start time)
|
||||
get_running_file() {
|
||||
local app="$1"
|
||||
echo "$STATE_DIR/${app}.running"
|
||||
}
|
||||
|
||||
# Clean up stale running state (process no longer running)
|
||||
cleanup_stale_running_state() {
|
||||
local app="$1"
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
if [[ ! -f $running_file ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid=$(awk '{print $1}' "$running_file" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z $pid ]]; then
|
||||
rm -f "$running_file"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if process is still running
|
||||
if ! kill -0 "$pid" 2>/dev/null; then
|
||||
log_message "CLEANUP: Stale running state for $app (PID $pid no longer exists)"
|
||||
rm -f "$running_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Launch app with auto-close timer
|
||||
launch_with_timer() {
|
||||
local app="$1"
|
||||
local real_binary="$2"
|
||||
shift 2
|
||||
|
||||
local warning_seconds=$(((AUTO_CLOSE_TIMEOUT_MINUTES - AUTO_CLOSE_WARNING_MINUTES) * 60))
|
||||
local running_file
|
||||
running_file=$(get_running_file "$app")
|
||||
|
||||
# Launch the app in background
|
||||
"$real_binary" "$@" &
|
||||
local app_pid=$!
|
||||
|
||||
# Record state
|
||||
echo "$app_pid $(date +%s)" >"$running_file"
|
||||
log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${AUTO_CLOSE_TIMEOUT_MINUTES}m)"
|
||||
|
||||
# Spawn the auto-close daemon in a completely detached subshell
|
||||
(
|
||||
# Detach from terminal
|
||||
exec </dev/null >/dev/null 2>&1
|
||||
|
||||
# Wait for warning time
|
||||
sleep "$warning_seconds"
|
||||
|
||||
# Check if still running before warning
|
||||
if kill -0 "$app_pid" 2>/dev/null; then
|
||||
# Send warning notification
|
||||
notify-send -u critical -t 30000 "⏰ $app Closing Soon" \
|
||||
"Session will end in ${AUTO_CLOSE_WARNING_MINUTES} minutes. Save your work!" 2>/dev/null || true
|
||||
else
|
||||
# Process already exited
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Wait remaining time
|
||||
sleep $((AUTO_CLOSE_WARNING_MINUTES * 60))
|
||||
|
||||
# Check if still running
|
||||
if kill -0 "$app_pid" 2>/dev/null; then
|
||||
# Send final notification
|
||||
notify-send -u critical -t 5000 "🚫 $app Session Ended" \
|
||||
"Time's up! Closing $app now." 2>/dev/null || true
|
||||
|
||||
# Graceful kill first
|
||||
kill "$app_pid" 2>/dev/null || true
|
||||
|
||||
# Wait a moment for graceful shutdown
|
||||
sleep 2
|
||||
|
||||
# Force kill if still running
|
||||
if kill -0 "$app_pid" 2>/dev/null; then
|
||||
kill -9 "$app_pid" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "$(date '+%Y-%m-%d %H:%M:%S') - AUTO-CLOSED: $app (PID $app_pid) after ${AUTO_CLOSE_TIMEOUT_MINUTES}m" >>"$LOG_FILE" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
) &
|
||||
disown
|
||||
|
||||
# Wait for the app to exit (keeps wrapper process alive while app is running)
|
||||
wait "$app_pid" 2>/dev/null || true
|
||||
local exit_code=$?
|
||||
|
||||
# Clean up running state
|
||||
rm -f "$running_file" 2>/dev/null || true
|
||||
|
||||
log_message "EXITED: $app (PID $app_pid) with code $exit_code"
|
||||
return $exit_code
|
||||
}
|
||||
|
||||
# Main wrapper function - called when wrapping app launches
|
||||
wrapper_main() {
|
||||
local app="$1"
|
||||
shift
|
||||
local app="$1"
|
||||
shift
|
||||
|
||||
ensure_state_dir
|
||||
ensure_state_dir
|
||||
|
||||
local real_binary
|
||||
if ! real_binary=$(get_real_binary "$app"); then
|
||||
log_message "ERROR: Real binary not found for $app"
|
||||
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
||||
exit 1
|
||||
fi
|
||||
local real_binary
|
||||
if ! real_binary=$(get_real_binary "$app"); then
|
||||
log_message "ERROR: Real binary not found for $app"
|
||||
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if was_opened_this_hour "$app"; then
|
||||
block_app "$app"
|
||||
exit 1
|
||||
fi
|
||||
# Clean up stale running state from previous crashes
|
||||
cleanup_stale_running_state "$app"
|
||||
|
||||
record_opening "$app"
|
||||
exec "$real_binary" "$@"
|
||||
if was_opened_this_hour "$app"; then
|
||||
block_app "$app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_opening "$app"
|
||||
|
||||
# Launch with auto-close timer (replaces direct exec)
|
||||
launch_with_timer "$app" "$real_binary" "$@"
|
||||
}
|
||||
|
||||
# Install wrapper for a specific app
|
||||
install_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local real_binary="${REAL_BINARIES[$app]}"
|
||||
|
||||
# Check if already wrapped
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ✓ $app already wrapped"
|
||||
return 0
|
||||
fi
|
||||
# Check if already wrapped
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ✓ $app already wrapped"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if wrapper location exists (file or symlink)
|
||||
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
||||
echo " ⚠ $app not installed ($wrapper_path not found)"
|
||||
return 1
|
||||
fi
|
||||
# Check if wrapper location exists (file or symlink)
|
||||
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
||||
echo " ⚠ $app not installed ($wrapper_path not found)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if real binary exists
|
||||
if [[ ! -x $real_binary ]]; then
|
||||
echo " ⚠ $app real binary not found ($real_binary)"
|
||||
return 1
|
||||
fi
|
||||
# Check if real binary exists
|
||||
if [[ ! -x $real_binary ]]; then
|
||||
echo " ⚠ $app real binary not found ($real_binary)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Installing wrapper for $app..."
|
||||
echo " Installing wrapper for $app..."
|
||||
|
||||
# Handle symlinks: save the symlink itself, not the target
|
||||
if [[ -L $wrapper_path ]]; then
|
||||
local link_target
|
||||
link_target=$(readlink "$wrapper_path")
|
||||
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
||||
# Remove symlink and create .orig that stores the link target info
|
||||
echo "SYMLINK:$link_target" > "${wrapper_path}.orig"
|
||||
rm "$wrapper_path"
|
||||
else
|
||||
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
||||
mv "$wrapper_path" "${wrapper_path}.orig"
|
||||
fi
|
||||
# Handle symlinks: save the symlink itself, not the target
|
||||
if [[ -L $wrapper_path ]]; then
|
||||
local link_target
|
||||
link_target=$(readlink "$wrapper_path")
|
||||
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
||||
# Remove symlink and create .orig that stores the link target info
|
||||
echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
|
||||
rm "$wrapper_path"
|
||||
else
|
||||
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
||||
mv "$wrapper_path" "${wrapper_path}.orig"
|
||||
fi
|
||||
|
||||
echo " Creating wrapper at $wrapper_path"
|
||||
cat > "$wrapper_path" << WRAPPER_EOF
|
||||
echo " Creating wrapper at $wrapper_path"
|
||||
cat >"$wrapper_path" <<WRAPPER_EOF
|
||||
#!/bin/bash
|
||||
# Auto-generated wrapper for $app - blocks compulsive opening
|
||||
# Real binary: $real_binary
|
||||
@ -195,88 +311,88 @@ install_wrapper() {
|
||||
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
|
||||
WRAPPER_EOF
|
||||
|
||||
chmod +x "$wrapper_path"
|
||||
echo " ✓ $app wrapper installed"
|
||||
chmod +x "$wrapper_path"
|
||||
echo " ✓ $app wrapper installed"
|
||||
}
|
||||
|
||||
# Uninstall wrapper for a specific app
|
||||
uninstall_wrapper() {
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
local app="$1"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ⚠ $app wrapper not found"
|
||||
return 1
|
||||
fi
|
||||
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
||||
echo " ⚠ $app wrapper not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Removing wrapper for $app..."
|
||||
rm -f "$wrapper_path"
|
||||
echo " Removing wrapper for $app..."
|
||||
rm -f "$wrapper_path"
|
||||
|
||||
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
||||
local orig_content
|
||||
orig_content=$(cat "${wrapper_path}.orig" 2> /dev/null || echo "")
|
||||
if [[ $orig_content == SYMLINK:* ]]; then
|
||||
local link_target="${orig_content#SYMLINK:}"
|
||||
echo " Restoring symlink $wrapper_path -> $link_target"
|
||||
ln -s "$link_target" "$wrapper_path"
|
||||
rm "${wrapper_path}.orig"
|
||||
else
|
||||
echo " Restoring original file"
|
||||
mv "${wrapper_path}.orig" "$wrapper_path"
|
||||
fi
|
||||
echo " ✓ $app restored"
|
||||
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
||||
local orig_content
|
||||
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
|
||||
if [[ $orig_content == SYMLINK:* ]]; then
|
||||
local link_target="${orig_content#SYMLINK:}"
|
||||
echo " Restoring symlink $wrapper_path -> $link_target"
|
||||
ln -s "$link_target" "$wrapper_path"
|
||||
rm "${wrapper_path}.orig"
|
||||
else
|
||||
echo " Restoring original file"
|
||||
mv "${wrapper_path}.orig" "$wrapper_path"
|
||||
fi
|
||||
echo " ✓ $app restored"
|
||||
}
|
||||
|
||||
# Install all wrappers
|
||||
install_all() {
|
||||
echo "Installing compulsive opening blockers..."
|
||||
echo ""
|
||||
echo "Installing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
# Install main script to /usr/local/bin
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
||||
# Install main script to /usr/local/bin
|
||||
local script_path
|
||||
script_path="$(readlink -f "$0")"
|
||||
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
if [[ $script_path != "$install_path" ]]; then
|
||||
echo "Installing main script to $install_path..."
|
||||
cp "$script_path" "$install_path"
|
||||
chmod +x "$install_path"
|
||||
echo "✓ Main script installed"
|
||||
else
|
||||
echo "Main script already at $install_path"
|
||||
fi
|
||||
echo ""
|
||||
if [[ $script_path != "$install_path" ]]; then
|
||||
echo "Installing main script to $install_path..."
|
||||
cp "$script_path" "$install_path"
|
||||
chmod +x "$install_path"
|
||||
echo "✓ Main script installed"
|
||||
else
|
||||
echo "Main script already at $install_path"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Install wrappers for each app
|
||||
local installed=0
|
||||
for app in "${!APPS[@]}"; do
|
||||
if install_wrapper "$app"; then
|
||||
((installed++)) || true
|
||||
fi
|
||||
done
|
||||
# Install wrappers for each app
|
||||
local installed=0
|
||||
for app in "${!APPS[@]}"; do
|
||||
if install_wrapper "$app"; then
|
||||
((installed++)) || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Installation complete. $installed app(s) wrapped."
|
||||
echo ""
|
||||
echo "Each app can now only be opened once per hour."
|
||||
echo "State files stored in: $STATE_DIR"
|
||||
echo "Logs stored in: $LOG_FILE"
|
||||
echo ""
|
||||
echo "Installation complete. $installed app(s) wrapped."
|
||||
echo ""
|
||||
echo "Each app can now only be opened once per hour."
|
||||
echo "State files stored in: $STATE_DIR"
|
||||
echo "Logs stored in: $LOG_FILE"
|
||||
|
||||
# Install pacman hook to re-wrap after package updates
|
||||
install_pacman_hook
|
||||
# Install pacman hook to re-wrap after package updates
|
||||
install_pacman_hook
|
||||
}
|
||||
|
||||
# Install pacman hook to re-install wrappers after package updates
|
||||
install_pacman_hook() {
|
||||
local hook_dir="/etc/pacman.d/hooks"
|
||||
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
||||
local hook_dir="/etc/pacman.d/hooks"
|
||||
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
||||
|
||||
echo ""
|
||||
echo "Installing pacman hook..."
|
||||
echo ""
|
||||
echo "Installing pacman hook..."
|
||||
|
||||
mkdir -p "$hook_dir"
|
||||
mkdir -p "$hook_dir"
|
||||
|
||||
cat > "$hook_file" << 'HOOK_EOF'
|
||||
cat >"$hook_file" <<'HOOK_EOF'
|
||||
[Trigger]
|
||||
Operation = Upgrade
|
||||
Operation = Install
|
||||
@ -291,131 +407,131 @@ When = PostTransaction
|
||||
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
||||
HOOK_EOF
|
||||
|
||||
chmod 644 "$hook_file"
|
||||
echo "✓ Pacman hook installed: $hook_file"
|
||||
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
||||
chmod 644 "$hook_file"
|
||||
echo "✓ Pacman hook installed: $hook_file"
|
||||
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
||||
}
|
||||
|
||||
# Uninstall pacman hook
|
||||
uninstall_pacman_hook() {
|
||||
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
||||
if [[ -f $hook_file ]]; then
|
||||
rm -f "$hook_file"
|
||||
echo "✓ Pacman hook removed"
|
||||
fi
|
||||
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
||||
if [[ -f $hook_file ]]; then
|
||||
rm -f "$hook_file"
|
||||
echo "✓ Pacman hook removed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Quietly re-wrap apps (for pacman hook - no interactive output)
|
||||
rewrap_quiet() {
|
||||
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
||||
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
for app in "${!APPS[@]}"; do
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
|
||||
# Check if wrapper was overwritten (no longer our wrapper script)
|
||||
if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2> /dev/null; then
|
||||
# Wrapper was overwritten by package update
|
||||
log_message "REWRAP: $app wrapper was overwritten, re-installing"
|
||||
# Check if wrapper was overwritten (no longer our wrapper script)
|
||||
if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||
# Wrapper was overwritten by package update
|
||||
log_message "REWRAP: $app wrapper was overwritten, re-installing"
|
||||
|
||||
# Remove old .orig if exists (it's now stale)
|
||||
rm -f "${wrapper_path}.orig"
|
||||
# Remove old .orig if exists (it's now stale)
|
||||
rm -f "${wrapper_path}.orig"
|
||||
|
||||
# Re-install wrapper
|
||||
install_wrapper "$app" >> "$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
# Re-install wrapper
|
||||
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
|
||||
fi
|
||||
done
|
||||
|
||||
log_message "REWRAP: Complete"
|
||||
log_message "REWRAP: Complete"
|
||||
}
|
||||
|
||||
# Uninstall all wrappers
|
||||
uninstall_all() {
|
||||
echo "Removing compulsive opening blockers..."
|
||||
echo ""
|
||||
echo "Removing compulsive opening blockers..."
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
uninstall_wrapper "$app" || true
|
||||
done
|
||||
for app in "${!APPS[@]}"; do
|
||||
uninstall_wrapper "$app" || true
|
||||
done
|
||||
|
||||
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
||||
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
||||
|
||||
# Remove pacman hook
|
||||
uninstall_pacman_hook
|
||||
# Remove pacman hook
|
||||
uninstall_pacman_hook
|
||||
|
||||
echo ""
|
||||
echo "Uninstallation complete."
|
||||
echo ""
|
||||
echo "Uninstallation complete."
|
||||
}
|
||||
|
||||
# Show status of all apps
|
||||
show_status() {
|
||||
ensure_state_dir
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
ensure_state_dir
|
||||
local current_hour
|
||||
current_hour=$(get_hour_key)
|
||||
|
||||
echo "Compulsive Opening Blocker Status"
|
||||
echo "=================================="
|
||||
echo "Current hour: $current_hour"
|
||||
echo ""
|
||||
echo "Compulsive Opening Blocker Status"
|
||||
echo "=================================="
|
||||
echo "Current hour: $current_hour"
|
||||
echo ""
|
||||
|
||||
for app in "${!APPS[@]}"; do
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local status="not opened this hour"
|
||||
local icon="○"
|
||||
for app in "${!APPS[@]}"; do
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local status="not opened this hour"
|
||||
local icon="○"
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
status="already opened (blocked until next hour)"
|
||||
icon="●"
|
||||
else
|
||||
status="last opened: $last_hour"
|
||||
fi
|
||||
fi
|
||||
if [[ -f $state_file ]]; then
|
||||
local last_hour
|
||||
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||
if [[ $last_hour == "$current_hour" ]]; then
|
||||
status="already opened (blocked until next hour)"
|
||||
icon="●"
|
||||
else
|
||||
status="last opened: $last_hour"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if wrapped
|
||||
local wrapped="not installed"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
wrapped="wrapped"
|
||||
elif [[ -f $wrapper_path ]]; then
|
||||
wrapped="installed (not wrapped)"
|
||||
fi
|
||||
# Check if wrapped
|
||||
local wrapped="not installed"
|
||||
local wrapper_path="${APPS[$app]}"
|
||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||
wrapped="wrapped"
|
||||
elif [[ -f $wrapper_path ]]; then
|
||||
wrapped="installed (not wrapped)"
|
||||
fi
|
||||
|
||||
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
||||
done
|
||||
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "State directory: $STATE_DIR"
|
||||
echo ""
|
||||
echo "State directory: $STATE_DIR"
|
||||
}
|
||||
|
||||
# Reset state for an app (allow opening again)
|
||||
reset_app() {
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
local app="$1"
|
||||
local state_file
|
||||
state_file=$(get_state_file "$app")
|
||||
|
||||
if [[ -f $state_file ]]; then
|
||||
rm -f "$state_file"
|
||||
echo "Reset $app - can be opened again this hour"
|
||||
log_message "RESET: $app state cleared by user"
|
||||
else
|
||||
echo "$app was not marked as opened"
|
||||
fi
|
||||
if [[ -f $state_file ]]; then
|
||||
rm -f "$state_file"
|
||||
echo "Reset $app - can be opened again this hour"
|
||||
log_message "RESET: $app state cleared by user"
|
||||
else
|
||||
echo "$app was not marked as opened"
|
||||
fi
|
||||
}
|
||||
|
||||
# Clear all state
|
||||
reset_all() {
|
||||
ensure_state_dir
|
||||
rm -f "$STATE_DIR"/*.lastopen
|
||||
echo "All apps reset - can be opened again this hour"
|
||||
log_message "RESET: All app states cleared by user"
|
||||
ensure_state_dir
|
||||
rm -f "$STATE_DIR"/*.lastopen
|
||||
echo "All apps reset - can be opened again this hour"
|
||||
log_message "RESET: All app states cleared by user"
|
||||
}
|
||||
|
||||
# Show usage
|
||||
show_usage() {
|
||||
cat << EOF
|
||||
cat <<EOF
|
||||
Block Compulsive Opening Script
|
||||
================================
|
||||
|
||||
@ -447,60 +563,60 @@ EOF
|
||||
|
||||
# Main entry point
|
||||
main() {
|
||||
case "${1:-help}" in
|
||||
install)
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Error: install requires root privileges"
|
||||
echo "Run: sudo $0 install"
|
||||
exit 1
|
||||
fi
|
||||
install_all
|
||||
;;
|
||||
uninstall)
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Error: uninstall requires root privileges"
|
||||
echo "Run: sudo $0 uninstall"
|
||||
exit 1
|
||||
fi
|
||||
uninstall_all
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
reset)
|
||||
if [[ -z ${2:-} ]]; then
|
||||
echo "Error: specify app to reset"
|
||||
echo "Apps: ${!APPS[*]}"
|
||||
exit 1
|
||||
fi
|
||||
reset_app "$2"
|
||||
;;
|
||||
reset-all)
|
||||
reset_all
|
||||
;;
|
||||
rewrap-quiet)
|
||||
# Called by pacman hook - quietly re-wrap apps after package updates
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
rewrap_quiet
|
||||
;;
|
||||
wrapper)
|
||||
if [[ -z ${2:-} ]]; then
|
||||
echo "Error: wrapper requires app name"
|
||||
exit 1
|
||||
fi
|
||||
wrapper_main "${@:2}"
|
||||
;;
|
||||
help | -h | --help)
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
case "${1:-help}" in
|
||||
install)
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Error: install requires root privileges"
|
||||
echo "Run: sudo $0 install"
|
||||
exit 1
|
||||
fi
|
||||
install_all
|
||||
;;
|
||||
uninstall)
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "Error: uninstall requires root privileges"
|
||||
echo "Run: sudo $0 uninstall"
|
||||
exit 1
|
||||
fi
|
||||
uninstall_all
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
reset)
|
||||
if [[ -z ${2:-} ]]; then
|
||||
echo "Error: specify app to reset"
|
||||
echo "Apps: ${!APPS[*]}"
|
||||
exit 1
|
||||
fi
|
||||
reset_app "$2"
|
||||
;;
|
||||
reset-all)
|
||||
reset_all
|
||||
;;
|
||||
rewrap-quiet)
|
||||
# Called by pacman hook - quietly re-wrap apps after package updates
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
rewrap_quiet
|
||||
;;
|
||||
wrapper)
|
||||
if [[ -z ${2:-} ]]; then
|
||||
echo "Error: wrapper requires app name"
|
||||
exit 1
|
||||
fi
|
||||
wrapper_main "${@:2}"
|
||||
;;
|
||||
help | -h | --help)
|
||||
show_usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $1"
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
275
scripts/digital_wellbeing/focus_mode_daemon.py
Executable file
275
scripts/digital_wellbeing/focus_mode_daemon.py
Executable file
@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Focus Mode Daemon - Steam/Browser Mutual Exclusion
|
||||
|
||||
This daemon monitors running processes and enforces mutual exclusion between
|
||||
Steam (gaming) and web browsers. Whichever starts first "wins" and the other
|
||||
category is blocked/killed.
|
||||
|
||||
Run as a systemd user service for continuous monitoring.
|
||||
"""
|
||||
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Set, Optional
|
||||
|
||||
# Configuration
|
||||
STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state")) / "focus-mode"
|
||||
LOG_FILE = STATE_DIR / "focus-mode.log"
|
||||
POLL_INTERVAL = 2 # seconds between process checks
|
||||
|
||||
# Process patterns
|
||||
STEAM_PATTERNS = frozenset([
|
||||
"steam",
|
||||
"steamwebhelper",
|
||||
"steam_ocompati", # Proton compatibility tool
|
||||
])
|
||||
|
||||
# Games often have steam_app_ prefix in process name
|
||||
STEAM_GAME_PREFIX = "steam_app_"
|
||||
|
||||
BROWSER_PATTERNS = frozenset([
|
||||
"firefox",
|
||||
"firefox-esr",
|
||||
"librewolf",
|
||||
"chromium",
|
||||
"chrome",
|
||||
"google-chrome",
|
||||
"brave",
|
||||
"vivaldi",
|
||||
"opera",
|
||||
"microsoft-edge",
|
||||
"ungoogled-chromium",
|
||||
])
|
||||
|
||||
# Patterns to ignore (browser helpers that aren't the main browser)
|
||||
IGNORE_PATTERNS = frozenset([
|
||||
"crashhandler",
|
||||
"update",
|
||||
"helper",
|
||||
])
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
"""Log message with timestamp."""
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_line = f"{timestamp} - {message}"
|
||||
print(log_line)
|
||||
try:
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(log_line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def notify(title: str, message: str, urgency: str = "normal") -> None:
|
||||
"""Send desktop notification."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-u", urgency, title, message],
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def get_running_processes() -> Set[str]:
|
||||
"""Get set of currently running process names."""
|
||||
processes = set()
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ps", "-eo", "comm="],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
proc_name = line.strip().lower()
|
||||
if proc_name:
|
||||
processes.add(proc_name)
|
||||
except Exception as e:
|
||||
log(f"Error getting processes: {e}")
|
||||
return processes
|
||||
|
||||
|
||||
def is_steam_running(processes: Set[str]) -> bool:
|
||||
"""Check if Steam or any Steam game is running."""
|
||||
for proc in processes:
|
||||
# Check for Steam main processes
|
||||
if proc in STEAM_PATTERNS:
|
||||
return True
|
||||
# Check for Steam games (have steam_app_ prefix)
|
||||
if proc.startswith(STEAM_GAME_PREFIX):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_browser_running(processes: Set[str]) -> bool:
|
||||
"""Check if any browser is running."""
|
||||
for proc in processes:
|
||||
# Skip ignored patterns
|
||||
if any(ign in proc for ign in IGNORE_PATTERNS):
|
||||
continue
|
||||
# Check browser patterns
|
||||
for pattern in BROWSER_PATTERNS:
|
||||
if pattern in proc:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def kill_steam() -> None:
|
||||
"""Kill all Steam-related processes."""
|
||||
log("Killing Steam processes...")
|
||||
notify("🎮 Gaming Blocked", "Browser is active. Closing Steam.", "critical")
|
||||
|
||||
try:
|
||||
# First try graceful shutdown
|
||||
subprocess.run(["pkill", "-f", "steam"], capture_output=True, timeout=5)
|
||||
time.sleep(2)
|
||||
|
||||
# Force kill if still running
|
||||
subprocess.run(["pkill", "-9", "-f", "steam"], capture_output=True, timeout=5)
|
||||
except Exception as e:
|
||||
log(f"Error killing Steam: {e}")
|
||||
|
||||
|
||||
def kill_browsers() -> None:
|
||||
"""Kill all browser processes."""
|
||||
log("Killing browser processes...")
|
||||
notify("🌐 Browsers Blocked", "Steam is active. Closing browsers.", "critical")
|
||||
|
||||
for browser in BROWSER_PATTERNS:
|
||||
try:
|
||||
subprocess.run(["pkill", "-f", browser], capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# Force kill if still running
|
||||
for browser in BROWSER_PATTERNS:
|
||||
try:
|
||||
subprocess.run(["pkill", "-9", "-f", browser], capture_output=True, timeout=5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class FocusMode:
|
||||
"""Tracks current focus mode and enforces mutual exclusion."""
|
||||
|
||||
def __init__(self):
|
||||
self.current_mode: Optional[str] = None # "gaming" or "browsing" or None
|
||||
self.mode_start_time: Optional[datetime] = None
|
||||
|
||||
def update(self, processes: Set[str]) -> None:
|
||||
"""Update focus mode based on running processes."""
|
||||
steam_running = is_steam_running(processes)
|
||||
browser_running = is_browser_running(processes)
|
||||
|
||||
if self.current_mode is None:
|
||||
# No mode set yet - first to start wins
|
||||
if steam_running and browser_running:
|
||||
# Both running at startup - prefer gaming mode (close browsers)
|
||||
log("Both Steam and browsers detected at startup - entering GAMING mode")
|
||||
self.current_mode = "gaming"
|
||||
self.mode_start_time = datetime.now()
|
||||
kill_browsers()
|
||||
elif steam_running:
|
||||
log("Steam detected - entering GAMING mode")
|
||||
self.current_mode = "gaming"
|
||||
self.mode_start_time = datetime.now()
|
||||
notify("🎮 Gaming Mode", "Steam detected. Browsers are now blocked.", "normal")
|
||||
elif browser_running:
|
||||
log("Browser detected - entering BROWSING mode")
|
||||
self.current_mode = "browsing"
|
||||
self.mode_start_time = datetime.now()
|
||||
notify("🌐 Browsing Mode", "Browser detected. Steam is now blocked.", "normal")
|
||||
|
||||
elif self.current_mode == "gaming":
|
||||
if not steam_running:
|
||||
# Steam closed - exit gaming mode
|
||||
log("Steam closed - exiting GAMING mode")
|
||||
self.current_mode = None
|
||||
self.mode_start_time = None
|
||||
notify("🎮 Gaming Mode Ended", "You can now use browsers.", "normal")
|
||||
elif browser_running:
|
||||
# Browser started while in gaming mode - kill it
|
||||
log("Browser detected during GAMING mode - killing browsers")
|
||||
kill_browsers()
|
||||
|
||||
elif self.current_mode == "browsing":
|
||||
if not browser_running:
|
||||
# Browsers closed - exit browsing mode
|
||||
log("Browsers closed - exiting BROWSING mode")
|
||||
self.current_mode = None
|
||||
self.mode_start_time = None
|
||||
notify("🌐 Browsing Mode Ended", "You can now use Steam.", "normal")
|
||||
elif steam_running:
|
||||
# Steam started while in browsing mode - kill it
|
||||
log("Steam detected during BROWSING mode - killing Steam")
|
||||
kill_steam()
|
||||
|
||||
def get_status(self) -> str:
|
||||
"""Get current status string."""
|
||||
if self.current_mode is None:
|
||||
return "No active focus mode"
|
||||
|
||||
duration = ""
|
||||
if self.mode_start_time:
|
||||
elapsed = datetime.now() - self.mode_start_time
|
||||
minutes = int(elapsed.total_seconds() // 60)
|
||||
duration = f" (active for {minutes}m)"
|
||||
|
||||
if self.current_mode == "gaming":
|
||||
return f"🎮 GAMING mode{duration} - browsers blocked"
|
||||
else:
|
||||
return f"🌐 BROWSING mode{duration} - Steam blocked"
|
||||
|
||||
|
||||
def write_status(focus: FocusMode) -> None:
|
||||
"""Write current status to state file for external queries."""
|
||||
try:
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
status_file = STATE_DIR / "status"
|
||||
with open(status_file, "w") as f:
|
||||
f.write(focus.get_status() + "\n")
|
||||
f.write(f"mode={focus.current_mode or 'none'}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""Main daemon loop."""
|
||||
log("Focus Mode Daemon starting...")
|
||||
|
||||
# Setup signal handlers
|
||||
def handle_signal(signum, frame):
|
||||
log(f"Received signal {signum} - shutting down")
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGTERM, handle_signal)
|
||||
signal.signal(signal.SIGINT, handle_signal)
|
||||
|
||||
focus = FocusMode()
|
||||
|
||||
while True:
|
||||
try:
|
||||
processes = get_running_processes()
|
||||
focus.update(processes)
|
||||
write_status(focus)
|
||||
except Exception as e:
|
||||
log(f"Error in main loop: {e}")
|
||||
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
211
scripts/digital_wellbeing/install_focus_mode_daemon.sh
Executable file
211
scripts/digital_wellbeing/install_focus_mode_daemon.sh
Executable file
@ -0,0 +1,211 @@
|
||||
#!/bin/bash
|
||||
# Install Focus Mode Daemon
|
||||
# Sets up Steam/Browser mutual exclusion as a systemd user service
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DAEMON_SCRIPT="$SCRIPT_DIR/focus_mode_daemon.py"
|
||||
INSTALL_PATH="/usr/local/bin/focus-mode-daemon"
|
||||
SERVICE_DIR="$HOME/.config/systemd/user"
|
||||
SERVICE_FILE="$SERVICE_DIR/focus-mode.service"
|
||||
|
||||
msg() { printf '\e[1;32m[+]\e[0m %s\n' "$*"; }
|
||||
note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; }
|
||||
warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; }
|
||||
err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Focus Mode Daemon Installer
|
||||
|
||||
Usage: $0 [install|uninstall|status]
|
||||
|
||||
Commands:
|
||||
install - Install and enable the focus mode daemon
|
||||
uninstall - Remove the daemon and disable the service
|
||||
status - Show current daemon status
|
||||
|
||||
The daemon enforces mutual exclusion between Steam and web browsers:
|
||||
- If Steam starts first: browsers are blocked/killed
|
||||
- If browser starts first: Steam is blocked/killed
|
||||
- Whichever started first "wins" until it exits
|
||||
EOF
|
||||
}
|
||||
|
||||
check_deps() {
|
||||
local missing=0
|
||||
|
||||
if ! command -v python3 &>/dev/null; then
|
||||
err "python3 is required but not installed"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if ! command -v systemctl &>/dev/null; then
|
||||
err "systemd is required but systemctl not found"
|
||||
missing=1
|
||||
fi
|
||||
|
||||
if [[ $missing -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install_daemon() {
|
||||
msg "Installing Focus Mode Daemon..."
|
||||
|
||||
check_deps
|
||||
|
||||
if [[ ! -f "$DAEMON_SCRIPT" ]]; then
|
||||
err "Daemon script not found: $DAEMON_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install the daemon script
|
||||
msg "Installing daemon script to $INSTALL_PATH"
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
install -m 755 "$DAEMON_SCRIPT" "$INSTALL_PATH"
|
||||
else
|
||||
sudo install -m 755 "$DAEMON_SCRIPT" "$INSTALL_PATH"
|
||||
fi
|
||||
|
||||
# Create systemd user directory
|
||||
mkdir -p "$SERVICE_DIR"
|
||||
|
||||
# Create the systemd user service
|
||||
msg "Creating systemd user service: $SERVICE_FILE"
|
||||
cat >"$SERVICE_FILE" <<'EOF'
|
||||
[Unit]
|
||||
Description=Focus Mode Daemon (Steam/Browser mutual exclusion)
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/focus-mode-daemon
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
# Don't allow easy stopping (psychological friction)
|
||||
RefuseManualStop=false
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
|
||||
# Reload systemd user daemon
|
||||
msg "Reloading systemd user daemon..."
|
||||
systemctl --user daemon-reload
|
||||
|
||||
# Enable and start the service
|
||||
msg "Enabling and starting focus-mode.service..."
|
||||
systemctl --user enable focus-mode.service
|
||||
systemctl --user start focus-mode.service
|
||||
|
||||
msg "Focus Mode Daemon installed successfully!"
|
||||
echo ""
|
||||
echo "The daemon is now running and will:"
|
||||
echo " 🎮 Block browsers when Steam is running"
|
||||
echo " 🌐 Block Steam when a browser is running"
|
||||
echo ""
|
||||
echo "Status: $(systemctl --user is-active focus-mode.service 2>/dev/null || echo 'unknown')"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " systemctl --user status focus-mode - Check daemon status"
|
||||
echo " journalctl --user -u focus-mode -f - View daemon logs"
|
||||
echo " cat ~/.local/state/focus-mode/status - View current mode"
|
||||
echo ""
|
||||
}
|
||||
|
||||
uninstall_daemon() {
|
||||
msg "Uninstalling Focus Mode Daemon..."
|
||||
|
||||
# Stop and disable service
|
||||
if systemctl --user is-active focus-mode.service &>/dev/null; then
|
||||
msg "Stopping focus-mode.service..."
|
||||
systemctl --user stop focus-mode.service || true
|
||||
fi
|
||||
|
||||
if systemctl --user is-enabled focus-mode.service &>/dev/null; then
|
||||
msg "Disabling focus-mode.service..."
|
||||
systemctl --user disable focus-mode.service || true
|
||||
fi
|
||||
|
||||
# Remove service file
|
||||
if [[ -f "$SERVICE_FILE" ]]; then
|
||||
msg "Removing service file..."
|
||||
rm -f "$SERVICE_FILE"
|
||||
fi
|
||||
|
||||
# Reload daemon
|
||||
systemctl --user daemon-reload 2>/dev/null || true
|
||||
|
||||
# Remove installed script
|
||||
if [[ -f "$INSTALL_PATH" ]]; then
|
||||
msg "Removing daemon script..."
|
||||
if [[ $EUID -eq 0 ]]; then
|
||||
rm -f "$INSTALL_PATH"
|
||||
else
|
||||
sudo rm -f "$INSTALL_PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
msg "Focus Mode Daemon uninstalled"
|
||||
note "State files in ~/.local/state/focus-mode/ were NOT removed"
|
||||
}
|
||||
|
||||
show_status() {
|
||||
echo "Focus Mode Daemon Status"
|
||||
echo "========================"
|
||||
echo ""
|
||||
|
||||
# Service status
|
||||
if systemctl --user is-active focus-mode.service &>/dev/null; then
|
||||
echo "Service: ✓ Running"
|
||||
else
|
||||
echo "Service: ✗ Not running"
|
||||
fi
|
||||
|
||||
if systemctl --user is-enabled focus-mode.service &>/dev/null; then
|
||||
echo "Enabled: ✓ Yes"
|
||||
else
|
||||
echo "Enabled: ✗ No"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Current mode
|
||||
local status_file="$HOME/.local/state/focus-mode/status"
|
||||
if [[ -f "$status_file" ]]; then
|
||||
echo "Current Mode:"
|
||||
cat "$status_file"
|
||||
else
|
||||
echo "Current Mode: Unknown (status file not found)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Recent Logs:"
|
||||
journalctl --user -u focus-mode --no-pager -n 10 2>/dev/null || echo " (no logs available)"
|
||||
}
|
||||
|
||||
# Main
|
||||
case "${1:-install}" in
|
||||
install)
|
||||
install_daemon
|
||||
;;
|
||||
uninstall)
|
||||
uninstall_daemon
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
-h | --help | help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
err "Unknown command: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
282
scripts/digital_wellbeing/pacman/README_FOR_LLM.md
Normal file
282
scripts/digital_wellbeing/pacman/README_FOR_LLM.md
Normal file
@ -0,0 +1,282 @@
|
||||
# Pacman Wrapper Security System - LLM Reference Guide
|
||||
|
||||
> **For AI assistants**: This document explains the pacman wrapper architecture so you can make correct modifications.
|
||||
|
||||
## System Purpose
|
||||
|
||||
Intercept all `pacman` commands to:
|
||||
1. Block installation of restricted packages (browsers, games, etc.)
|
||||
2. Require challenges for greylisted packages
|
||||
3. Enforce hosts file sharing on VirtualBox VMs
|
||||
4. Auto-setup maintenance services if missing
|
||||
5. Handle stale database locks gracefully
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ PACMAN WRAPPER │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ User runs: pacman -S firefox │
|
||||
│ ↓ │
|
||||
│ /usr/bin/pacman (symlink) → pacman_wrapper.sh │
|
||||
│ ↓ │
|
||||
│ 1. Verify policy file integrity (SHA256) │
|
||||
│ 2. Check if package matches blocked keywords │
|
||||
│ 3. Check if package requires challenge (greylist) │
|
||||
│ 4. Run hosts-guard pre-unlock hook │
|
||||
│ 5. Execute real pacman: /usr/bin/pacman.orig │
|
||||
│ 6. Run hosts-guard post-relock hook │
|
||||
│ 7. Remove any blocked packages that slipped through │
|
||||
│ 8. Enforce VirtualBox hosts if vbox detected │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `/usr/bin/pacman` | Symlink to wrapper |
|
||||
| `/usr/bin/pacman.orig` | Real pacman binary |
|
||||
| `pacman_wrapper.sh` | Main wrapper script (823 lines) |
|
||||
| `install_pacman_wrapper.sh` | Installer script |
|
||||
| `pacman_blocked_keywords.txt` | Substrings that cause blocking |
|
||||
| `pacman_whitelist.txt` | Exact names that bypass blocking |
|
||||
| `pacman_greylist.txt` | Packages requiring challenge |
|
||||
| `words.txt` | Word scramble challenge dictionary |
|
||||
| `/var/lib/pacman-wrapper/policy.sha256` | Integrity checksums |
|
||||
|
||||
## Policy Files Explained
|
||||
|
||||
### pacman_blocked_keywords.txt
|
||||
```
|
||||
# Lines starting with # are comments
|
||||
# Any package containing these substrings is BLOCKED
|
||||
firefox
|
||||
brave
|
||||
chromium
|
||||
youtube
|
||||
stremio
|
||||
```
|
||||
|
||||
If user tries `pacman -S firefox-developer-edition`, it's blocked because it contains "firefox".
|
||||
|
||||
### pacman_whitelist.txt
|
||||
```
|
||||
# Exact package names that bypass keyword blocking
|
||||
minizip # Contains nothing bad but might match a pattern
|
||||
python-requests # Safe despite containing blocked substrings
|
||||
```
|
||||
|
||||
### pacman_greylist.txt
|
||||
```
|
||||
# Packages requiring word scramble challenge
|
||||
# Currently empty - add packages here for challenge requirement
|
||||
```
|
||||
|
||||
## Hardcoded Security Checks
|
||||
|
||||
These checks are in the script itself and **cannot be bypassed by editing policy files**:
|
||||
|
||||
### VirtualBox Check
|
||||
```bash
|
||||
function is_virtualbox_package() {
|
||||
local pkg_lower="${1,,}"
|
||||
[[ $pkg_lower == *"virtualbox"* || $pkg_lower == *"vbox"* ]]
|
||||
}
|
||||
```
|
||||
- Detects any package with "virtualbox" or "vbox" in name
|
||||
- Requires word scramble challenge (7-letter words, 120s timeout)
|
||||
- Auto-enforces hosts file sharing on all VMs after install
|
||||
|
||||
### Steam Check
|
||||
```bash
|
||||
function is_steam_package() {
|
||||
[[ $1 == "steam" ]]
|
||||
}
|
||||
```
|
||||
- Only exact match "steam" (not steam-native-runtime etc.)
|
||||
- **Weekend only** - blocked Monday through Friday 4PM
|
||||
- Requires word scramble challenge (5-letter words, 60s timeout)
|
||||
|
||||
## Word Scramble Challenge
|
||||
|
||||
Used for Steam, VirtualBox, and greylisted packages:
|
||||
|
||||
```
|
||||
Challenge: Words with 5 letters
|
||||
Here are 160 random words. Remember them:
|
||||
APPLE BRAVE CHAIR DANCE ...
|
||||
|
||||
One of those words has been scrambled to: ELPPA
|
||||
Unscramble the word to proceed (you have 60 seconds):
|
||||
```
|
||||
|
||||
Parameters vary by package type:
|
||||
| Package Type | Word Length | Words Shown | Timeout | Initial Delay |
|
||||
|--------------|-------------|-------------|---------|---------------|
|
||||
| Steam | 5 | 160 | 60s | 0-20s |
|
||||
| VirtualBox | 7 | 150 | 120s | 0-45s |
|
||||
| Greylist | 6 | 120 | 90s | 0-30s |
|
||||
|
||||
## Integrity Verification
|
||||
|
||||
On every invocation, the wrapper verifies policy files haven't been tampered with:
|
||||
|
||||
```bash
|
||||
verify_policy_integrity() {
|
||||
# Reads /var/lib/pacman-wrapper/policy.sha256
|
||||
# Compares SHA256 of each policy file
|
||||
# If mismatch: BLOCKS all operations
|
||||
}
|
||||
```
|
||||
|
||||
If tampering detected:
|
||||
```
|
||||
SECURITY WARNING: Policy file integrity check failed!
|
||||
CRITICAL: Policy files have been tampered with!
|
||||
Wrapper operation DENIED. Please reinstall using: sudo install_pacman_wrapper.sh
|
||||
```
|
||||
|
||||
## Hosts Integration
|
||||
|
||||
The wrapper integrates with the hosts guard system:
|
||||
|
||||
```bash
|
||||
pre_unlock_hosts() {
|
||||
# Called before any transaction (-S, -U, -R)
|
||||
/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh
|
||||
}
|
||||
|
||||
post_relock_hosts() {
|
||||
# Called after transaction completes
|
||||
/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh
|
||||
}
|
||||
```
|
||||
|
||||
This allows package installations to modify `/etc/hosts` temporarily (e.g., for network setup) while maintaining protection.
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding a Blocked Package
|
||||
|
||||
1. Edit `pacman_blocked_keywords.txt`:
|
||||
```bash
|
||||
echo "newkeyword" >> pacman_blocked_keywords.txt
|
||||
```
|
||||
|
||||
2. Reinstall wrapper to update checksums:
|
||||
```bash
|
||||
sudo ./install_pacman_wrapper.sh
|
||||
```
|
||||
|
||||
### Whitelisting a Package
|
||||
|
||||
If a legitimate package is being blocked (e.g., `python-firefox-sync` blocked by "firefox" keyword):
|
||||
|
||||
1. Edit `pacman_whitelist.txt`:
|
||||
```bash
|
||||
echo "python-firefox-sync" >> pacman_whitelist.txt
|
||||
```
|
||||
|
||||
2. Reinstall wrapper:
|
||||
```bash
|
||||
sudo ./install_pacman_wrapper.sh
|
||||
```
|
||||
|
||||
### Adding a Challenge Requirement
|
||||
|
||||
1. Edit `pacman_greylist.txt`:
|
||||
```bash
|
||||
echo "suspicious-package" >> pacman_greylist.txt
|
||||
```
|
||||
|
||||
2. Reinstall wrapper.
|
||||
|
||||
### Bypassing the Wrapper (Emergency)
|
||||
|
||||
If wrapper is broken and you need real pacman:
|
||||
```bash
|
||||
sudo /usr/bin/pacman.orig -S package
|
||||
```
|
||||
|
||||
**Warning**: This bypasses all security checks.
|
||||
|
||||
## Post-Transaction Cleanup
|
||||
|
||||
After every transaction, the wrapper:
|
||||
|
||||
1. Scans installed packages for blocked keywords
|
||||
2. Removes any that match (shouldn't happen normally)
|
||||
3. Scans for greylisted packages and removes them
|
||||
4. Checks if VirtualBox is installed and enforces hosts
|
||||
|
||||
```bash
|
||||
remove_installed_blocked_packages() {
|
||||
mapfile -t installed_names < <("$PACMAN_BIN" -Qq)
|
||||
for name in "${installed_names[@]}"; do
|
||||
if is_blocked_package_name "$name"; then
|
||||
pacman -Rns --noconfirm "$name"
|
||||
fi
|
||||
done
|
||||
}
|
||||
```
|
||||
|
||||
## Stale Lock Handling
|
||||
|
||||
If `/var/lib/pacman/db.lck` exists but no pacman is running:
|
||||
- Interactive: Prompts user to remove (15s timeout)
|
||||
- Non-interactive (`--noconfirm`): Auto-removes if lock is >10 minutes old
|
||||
- If another pacman is actually running: Blocks with error
|
||||
|
||||
## Maintenance Auto-Setup
|
||||
|
||||
On first run, wrapper checks if periodic maintenance services exist:
|
||||
```bash
|
||||
ensure_periodic_maintenance() {
|
||||
# Checks: periodic-system-maintenance.timer
|
||||
# periodic-system-startup.service
|
||||
# hosts-file-monitor.service
|
||||
# If missing: runs setup_periodic_system.sh
|
||||
}
|
||||
```
|
||||
|
||||
## Known Gaps (TODO)
|
||||
|
||||
1. ❌ `google-chrome` and `google-chrome-stable` not in blocked list
|
||||
2. ❌ No automatic LeechBlock installation when browsers detected
|
||||
3. ❌ User can download and install `.deb`/`.tar.gz` manually
|
||||
4. ❌ AUR packages bypass wrapper (yay/paru call pacman internally)
|
||||
|
||||
## Debugging
|
||||
|
||||
### Check if wrapper is installed
|
||||
```bash
|
||||
ls -la /usr/bin/pacman
|
||||
# Should show: /usr/bin/pacman -> /path/to/pacman_wrapper.sh
|
||||
|
||||
ls -la /usr/bin/pacman.orig
|
||||
# Should exist and be the real binary
|
||||
```
|
||||
|
||||
### Test policy integrity
|
||||
```bash
|
||||
cat /var/lib/pacman-wrapper/policy.sha256
|
||||
sha256sum /path/to/pacman_blocked_keywords.txt
|
||||
# Hashes should match
|
||||
```
|
||||
|
||||
### Verbose mode
|
||||
The wrapper outputs colored status messages to stderr. To see them:
|
||||
```bash
|
||||
pacman -S package 2>&1 | cat
|
||||
```
|
||||
|
||||
## DO NOT
|
||||
|
||||
1. ❌ Edit policy files without reinstalling wrapper (breaks integrity check)
|
||||
2. ❌ Remove `/usr/bin/pacman.orig` (breaks all pacman operations)
|
||||
3. ❌ Symlink pacman to something other than the wrapper
|
||||
4. ❌ Clear `/var/lib/pacman-wrapper/` without understanding consequences
|
||||
@ -53,4 +53,8 @@ netsurf
|
||||
amfora
|
||||
tartube
|
||||
youtube
|
||||
virtualbox
|
||||
# Chrome/Chromium variants
|
||||
google-chrome
|
||||
chromium
|
||||
ungoogled-chromium
|
||||
thorium
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
# Packages matching any of these substrings require a challenge to install.
|
||||
# They will also be uninstalled if found already installed.
|
||||
# Lines starting with # are comments.
|
||||
virtualbox
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
22
scripts/fixes/fix_waifu2x.sh
Executable file
22
scripts/fixes/fix_waifu2x.sh
Executable file
@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Fix waifu2x-converter-cpp-cuda-git for CUDA 13+
|
||||
# CUDA 13 minimum supported arch is sm_75 (Turing)
|
||||
|
||||
PKGBUILD="$HOME/.cache/yay/waifu2x-converter-cpp-cuda-git/PKGBUILD"
|
||||
|
||||
if [[ ! -f "$PKGBUILD" ]]; then
|
||||
echo "PKGBUILD not found. Run 'yay waifu2x-converter-cpp-cuda-git' first to download it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add sed commands to prepare() function to replace sm_52/ptx52 with sm_75/ptx75
|
||||
if grep -q 's/sm_52/sm_75' "$PKGBUILD"; then
|
||||
echo "PKGBUILD already patched."
|
||||
else
|
||||
sed -i '/^prepare() {$/a\
|
||||
# Fix for CUDA 13+ which requires sm_75+ (Turing)\
|
||||
sed -i "s/sm_52/sm_75/g" waifu2x-converter-cpp/CMakeLists.txt\
|
||||
sed -i "s/ptx52/ptx75/g" waifu2x-converter-cpp/CMakeLists.txt\
|
||||
sed -i "s/ptx52/ptx75/g" waifu2x-converter-cpp/src/modelHandler_CUDA.cpp' "$PKGBUILD"
|
||||
echo "PKGBUILD patched. Now run 'yay waifu2x-converter-cpp-cuda-git' again."
|
||||
fi
|
||||
8
test_results.log
Normal file
8
test_results.log
Normal file
@ -0,0 +1,8 @@
|
||||
==========================================
|
||||
Security Hardening Test Suite
|
||||
==========================================
|
||||
|
||||
Testing components in: /home/kuhy/linux-configuration
|
||||
|
||||
--- HOSTS GUARD ---
|
||||
[0;32m✅ PASS[0m: /etc/hosts is immutable
|
||||
385
tests/test_security_hardening.sh
Executable file
385
tests/test_security_hardening.sh
Executable file
@ -0,0 +1,385 @@
|
||||
#!/bin/bash
|
||||
# tests/test_security_hardening.sh
|
||||
# Verify all security mechanisms are working
|
||||
#
|
||||
# Run with: bash tests/test_security_hardening.sh
|
||||
# Some tests require root privileges
|
||||
|
||||
set -uo pipefail
|
||||
# Note: NOT using -e because we need to handle test failures gracefully
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
SKIP=0
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
test_result() {
|
||||
local name="$1"
|
||||
local result="$2"
|
||||
local reason="${3:-}"
|
||||
|
||||
case "$result" in
|
||||
pass)
|
||||
echo -e "${GREEN}✅ PASS${NC}: $name"
|
||||
((PASS++))
|
||||
;;
|
||||
fail)
|
||||
echo -e "${RED}❌ FAIL${NC}: $name"
|
||||
[[ -n "$reason" ]] && echo -e " ${RED}Reason: $reason${NC}"
|
||||
((FAIL++))
|
||||
;;
|
||||
skip)
|
||||
echo -e "${YELLOW}⏭️ SKIP${NC}: $name"
|
||||
[[ -n "$reason" ]] && echo -e " ${YELLOW}Reason: $reason${NC}"
|
||||
((SKIP++))
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Security Hardening Test Suite"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Testing components in: $REPO_DIR"
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# HOSTS GUARD TESTS
|
||||
# ==================================================================
|
||||
echo "--- HOSTS GUARD ---"
|
||||
|
||||
# Test 1: /etc/hosts is immutable
|
||||
if [[ -f /etc/hosts ]]; then
|
||||
if lsattr /etc/hosts 2>/dev/null | grep -q '^....i'; then
|
||||
test_result "/etc/hosts is immutable" "pass"
|
||||
else
|
||||
test_result "/etc/hosts is immutable" "fail" "chattr +i not set"
|
||||
fi
|
||||
else
|
||||
test_result "/etc/hosts is immutable" "skip" "File not found"
|
||||
fi
|
||||
|
||||
# Test 2: hosts-guard.path is active
|
||||
if systemctl is-active --quiet hosts-guard.path 2>/dev/null; then
|
||||
test_result "hosts-guard.path is active" "pass"
|
||||
else
|
||||
test_result "hosts-guard.path is active" "fail" "Service not running"
|
||||
fi
|
||||
|
||||
# Test 3: hosts-bind-mount.service is active
|
||||
if systemctl is-active --quiet hosts-bind-mount.service 2>/dev/null; then
|
||||
test_result "hosts-bind-mount.service is active" "pass"
|
||||
else
|
||||
test_result "hosts-bind-mount.service is active" "fail" "Service not running"
|
||||
fi
|
||||
|
||||
# Test 4: Canonical hosts copy exists
|
||||
if [[ -f /usr/local/share/locked-hosts ]]; then
|
||||
test_result "Canonical hosts copy exists" "pass"
|
||||
else
|
||||
test_result "Canonical hosts copy exists" "fail" "Not found at /usr/local/share/locked-hosts"
|
||||
fi
|
||||
|
||||
# Test 5: nsswitch-guard.path is active (NEW)
|
||||
if systemctl is-active --quiet nsswitch-guard.path 2>/dev/null; then
|
||||
test_result "nsswitch-guard.path is active" "pass"
|
||||
else
|
||||
test_result "nsswitch-guard.path is active" "fail" "Service not running"
|
||||
fi
|
||||
|
||||
# Test 6: /etc/nsswitch.conf is immutable (NEW)
|
||||
if [[ -f /etc/nsswitch.conf ]]; then
|
||||
if lsattr /etc/nsswitch.conf 2>/dev/null | grep -q '^....i'; then
|
||||
test_result "/etc/nsswitch.conf is immutable" "pass"
|
||||
else
|
||||
test_result "/etc/nsswitch.conf is immutable" "fail" "chattr +i not set"
|
||||
fi
|
||||
else
|
||||
test_result "/etc/nsswitch.conf is immutable" "skip" "File not found"
|
||||
fi
|
||||
|
||||
# Test 7: nsswitch.conf has correct hosts line
|
||||
if [[ -f /etc/nsswitch.conf ]]; then
|
||||
hosts_line=$(grep "^hosts:" /etc/nsswitch.conf 2>/dev/null || true)
|
||||
if echo "$hosts_line" | grep -q 'files.*dns\|files.*myhostname'; then
|
||||
test_result "nsswitch.conf has 'files' before 'dns'" "pass"
|
||||
elif [[ -z "$hosts_line" ]]; then
|
||||
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "No hosts: line found"
|
||||
else
|
||||
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "hosts line: $hosts_line"
|
||||
fi
|
||||
else
|
||||
test_result "nsswitch.conf has 'files' before 'dns'" "skip" "File not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# SHUTDOWN SCHEDULE TESTS
|
||||
# ==================================================================
|
||||
echo "--- SHUTDOWN SCHEDULE ---"
|
||||
|
||||
# Test 8: shutdown-schedule.conf is immutable
|
||||
if [[ -f /etc/shutdown-schedule.conf ]]; then
|
||||
if lsattr /etc/shutdown-schedule.conf 2>/dev/null | grep -q '^....i'; then
|
||||
test_result "/etc/shutdown-schedule.conf is immutable" "pass"
|
||||
else
|
||||
test_result "/etc/shutdown-schedule.conf is immutable" "fail" "chattr +i not set"
|
||||
fi
|
||||
else
|
||||
test_result "/etc/shutdown-schedule.conf is immutable" "skip" "Not installed"
|
||||
fi
|
||||
|
||||
# Test 9: shutdown timer is active
|
||||
if systemctl is-active --quiet day-specific-shutdown.timer 2>/dev/null; then
|
||||
test_result "day-specific-shutdown.timer is active" "pass"
|
||||
else
|
||||
test_result "day-specific-shutdown.timer is active" "fail" "Timer not running"
|
||||
fi
|
||||
|
||||
# Test 10: shutdown schedule guard is active
|
||||
if systemctl is-active --quiet shutdown-schedule-guard.path 2>/dev/null; then
|
||||
test_result "shutdown-schedule-guard.path is active" "pass"
|
||||
else
|
||||
test_result "shutdown-schedule-guard.path is active" "fail" "Guard not running"
|
||||
fi
|
||||
|
||||
# Test 11: Unlock script has obscure name (no helpful path)
|
||||
if [[ -f /usr/local/sbin/.sd-sched-mgmt ]]; then
|
||||
test_result "Unlock script uses obscure name" "pass"
|
||||
elif [[ -f /usr/local/sbin/unlock-shutdown-schedule ]]; then
|
||||
test_result "Unlock script uses obscure name" "fail" "Still using obvious name"
|
||||
else
|
||||
test_result "Unlock script uses obscure name" "skip" "Not installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# PACMAN WRAPPER TESTS
|
||||
# ==================================================================
|
||||
echo "--- PACMAN WRAPPER ---"
|
||||
|
||||
# Test 12: pacman wrapper is installed
|
||||
if [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]]; then
|
||||
test_result "pacman wrapper installed" "pass"
|
||||
else
|
||||
if [[ ! -L /usr/bin/pacman ]]; then
|
||||
test_result "pacman wrapper installed" "fail" "/usr/bin/pacman is not a symlink"
|
||||
else
|
||||
test_result "pacman wrapper installed" "fail" "/usr/bin/pacman.orig not found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test 13: google-chrome is blocked
|
||||
blocked_file="$REPO_DIR/scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt"
|
||||
if [[ -f "$blocked_file" ]]; then
|
||||
if grep -qi "google-chrome" "$blocked_file"; then
|
||||
test_result "google-chrome in blocked list" "pass"
|
||||
else
|
||||
test_result "google-chrome in blocked list" "fail" "Not found in $blocked_file"
|
||||
fi
|
||||
else
|
||||
test_result "google-chrome in blocked list" "skip" "Blocked keywords file not found"
|
||||
fi
|
||||
|
||||
# Test 14: chromium is blocked
|
||||
if [[ -f "$blocked_file" ]]; then
|
||||
if grep -qi "^chromium$" "$blocked_file"; then
|
||||
test_result "chromium in blocked list" "pass"
|
||||
else
|
||||
test_result "chromium in blocked list" "fail" "Not found in $blocked_file"
|
||||
fi
|
||||
else
|
||||
test_result "chromium in blocked list" "skip" "Blocked keywords file not found"
|
||||
fi
|
||||
|
||||
# Test 15: Policy integrity file exists
|
||||
if [[ -f /var/lib/pacman-wrapper/policy.sha256 ]]; then
|
||||
test_result "Pacman policy integrity file exists" "pass"
|
||||
else
|
||||
test_result "Pacman policy integrity file exists" "fail" "Not found"
|
||||
fi
|
||||
|
||||
# Test 16: LeechBlock auto-install function exists in wrapper
|
||||
wrapper_file="$REPO_DIR/scripts/digital_wellbeing/pacman/pacman_wrapper.sh"
|
||||
if [[ -f "$wrapper_file" ]]; then
|
||||
if grep -q "auto_install_leechblock" "$wrapper_file"; then
|
||||
test_result "LeechBlock auto-install function exists" "pass"
|
||||
else
|
||||
test_result "LeechBlock auto-install function exists" "fail" "Function not found"
|
||||
fi
|
||||
else
|
||||
test_result "LeechBlock auto-install function exists" "skip" "Wrapper file not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# COMPULSIVE BLOCK TESTS
|
||||
# ==================================================================
|
||||
echo "--- COMPULSIVE OPENING BLOCK ---"
|
||||
|
||||
compulsive_file="$REPO_DIR/scripts/digital_wellbeing/block_compulsive_opening.sh"
|
||||
|
||||
# Test 17: Auto-close timer configuration exists
|
||||
if [[ -f "$compulsive_file" ]]; then
|
||||
if grep -q "AUTO_CLOSE_TIMEOUT_MINUTES" "$compulsive_file"; then
|
||||
test_result "Auto-close timer configuration exists" "pass"
|
||||
else
|
||||
test_result "Auto-close timer configuration exists" "fail" "Variable not found"
|
||||
fi
|
||||
else
|
||||
test_result "Auto-close timer configuration exists" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
# Test 18: launch_with_timer function exists
|
||||
if [[ -f "$compulsive_file" ]]; then
|
||||
if grep -q "launch_with_timer" "$compulsive_file"; then
|
||||
test_result "launch_with_timer function exists" "pass"
|
||||
else
|
||||
test_result "launch_with_timer function exists" "fail" "Function not found"
|
||||
fi
|
||||
else
|
||||
test_result "launch_with_timer function exists" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
# Test 19: Compulsive block wrappers installed
|
||||
wrappers_ok=true
|
||||
for app in beeper signal-desktop discord; do
|
||||
if [[ -f "/usr/bin/$app" ]]; then
|
||||
if grep -q "block-compulsive-opening" "/usr/bin/$app" 2>/dev/null; then
|
||||
: # OK
|
||||
else
|
||||
wrappers_ok=false
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [[ "$wrappers_ok" == true ]]; then
|
||||
test_result "Compulsive block wrappers installed" "pass"
|
||||
else
|
||||
test_result "Compulsive block wrappers installed" "fail" "Some wrappers missing or incorrect"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# FOCUS MODE DAEMON TESTS
|
||||
# ==================================================================
|
||||
echo "--- FOCUS MODE DAEMON ---"
|
||||
|
||||
focus_daemon="$REPO_DIR/scripts/digital_wellbeing/focus_mode_daemon.py"
|
||||
focus_installer="$REPO_DIR/scripts/digital_wellbeing/install_focus_mode_daemon.sh"
|
||||
|
||||
# Test 20: Focus mode daemon script exists
|
||||
if [[ -f "$focus_daemon" ]]; then
|
||||
test_result "Focus mode daemon script exists" "pass"
|
||||
else
|
||||
test_result "Focus mode daemon script exists" "fail" "Not found at $focus_daemon"
|
||||
fi
|
||||
|
||||
# Test 21: Focus mode installer exists
|
||||
if [[ -f "$focus_installer" ]]; then
|
||||
test_result "Focus mode installer exists" "pass"
|
||||
else
|
||||
test_result "Focus mode installer exists" "fail" "Not found at $focus_installer"
|
||||
fi
|
||||
|
||||
# Test 22: Focus mode daemon is running (user service)
|
||||
if systemctl --user is-active --quiet focus-mode.service 2>/dev/null; then
|
||||
test_result "Focus mode daemon is running" "pass"
|
||||
else
|
||||
test_result "Focus mode daemon is running" "fail" "User service not running"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# SCREEN LOCKER TESTS
|
||||
# ==================================================================
|
||||
echo "--- SCREEN LOCKER ---"
|
||||
|
||||
screen_locker="$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py"
|
||||
|
||||
# Test 23: Screen locker exists
|
||||
if [[ -f "$screen_locker" ]]; then
|
||||
test_result "Screen locker script exists" "pass"
|
||||
else
|
||||
test_result "Screen locker script exists" "skip" "Not found at expected location"
|
||||
fi
|
||||
|
||||
# Test 24: Running option removed
|
||||
if [[ -f "$screen_locker" ]]; then
|
||||
# Check that there's no "Running" button in ask_workout_type
|
||||
if grep -A 20 "def ask_workout_type" "$screen_locker" | grep -q '"Running"'; then
|
||||
test_result "Running workout option removed" "fail" "Still present in ask_workout_type"
|
||||
else
|
||||
test_result "Running workout option removed" "pass"
|
||||
fi
|
||||
else
|
||||
test_result "Running workout option removed" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
# Test 25: Table tennis minimum sets validation
|
||||
if [[ -f "$screen_locker" ]]; then
|
||||
if grep -q "MIN_TABLE_TENNIS_SETS" "$screen_locker"; then
|
||||
test_result "Table tennis minimum sets validation exists" "pass"
|
||||
else
|
||||
test_result "Table tennis minimum sets validation exists" "fail" "Constant not found"
|
||||
fi
|
||||
else
|
||||
test_result "Table tennis minimum sets validation exists" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
# Test 26: Table tennis verification question
|
||||
if [[ -f "$screen_locker" ]]; then
|
||||
if grep -q "ask_table_tennis_verification" "$screen_locker"; then
|
||||
test_result "Table tennis verification question exists" "pass"
|
||||
else
|
||||
test_result "Table tennis verification question exists" "fail" "Function not found"
|
||||
fi
|
||||
else
|
||||
test_result "Table tennis verification question exists" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
# Test 27: 60 second submit delay for table tennis
|
||||
if [[ -f "$screen_locker" ]]; then
|
||||
if grep -q "TABLE_TENNIS_SUBMIT_DELAY = 60" "$screen_locker"; then
|
||||
test_result "Table tennis 60-second submit delay" "pass"
|
||||
else
|
||||
test_result "Table tennis 60-second submit delay" "fail" "Constant not set to 60"
|
||||
fi
|
||||
else
|
||||
test_result "Table tennis 60-second submit delay" "skip" "Script not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ==================================================================
|
||||
# SUMMARY
|
||||
# ==================================================================
|
||||
echo "=========================================="
|
||||
echo "RESULTS SUMMARY"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo -e "${GREEN}Passed: $PASS${NC}"
|
||||
echo -e "${RED}Failed: $FAIL${NC}"
|
||||
echo -e "${YELLOW}Skipped: $SKIP${NC}"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
if [[ $FAIL -gt 0 ]]; then
|
||||
echo -e "${RED}Some tests failed! Review the output above.${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}All active tests passed!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
Loading…
Reference in New Issue
Block a user