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
|
||||
0
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
0
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
0
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
0
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
1
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh
Normal file → Executable file
1
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
|
||||
|
||||
0
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
0
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
@ -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
|
||||
@ -84,6 +85,10 @@ while [[ $# -gt 0 ]]; do
|
||||
ENABLE_PATH=0
|
||||
shift
|
||||
;;
|
||||
--skip-nsswitch)
|
||||
ENABLE_NSSWITCH=0
|
||||
shift
|
||||
;;
|
||||
--delay)
|
||||
DELAY=${2:-}
|
||||
[[ -z ${DELAY} ]] && {
|
||||
@ -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"
|
||||
@ -166,7 +177,7 @@ SYSTEMD_DIR="/etc/systemd/system"
|
||||
######################################################################
|
||||
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
|
||||
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
|
||||
@ -174,14 +185,17 @@ if [[ $UNINSTALL -eq 1 ]]; then
|
||||
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 snapshot at $CANON (remove manually if undesired)."
|
||||
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
|
||||
@ -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
|
||||
|
||||
@ -373,7 +389,34 @@ else
|
||||
note "Skipping bind mount (--skip-bind)"
|
||||
fi
|
||||
|
||||
msg "Performing initial enforcement"
|
||||
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
|
||||
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
|
||||
@ -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)"
|
||||
|
||||
109
hosts/install.sh
109
hosts/install.sh
@ -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
|
||||
@ -420,7 +420,98 @@ else
|
||||
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)
|
||||
@ -26,6 +26,11 @@ notify() {
|
||||
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=(
|
||||
@ -124,6 +129,112 @@ get_real_binary() {
|
||||
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"
|
||||
@ -138,13 +249,18 @@ wrapper_main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up stale running state from previous crashes
|
||||
cleanup_stale_running_state "$app"
|
||||
|
||||
if was_opened_this_hour "$app"; then
|
||||
block_app "$app"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
record_opening "$app"
|
||||
exec "$real_binary" "$@"
|
||||
|
||||
# Launch with auto-close timer (replaces direct exec)
|
||||
launch_with_timer "$app" "$real_binary" "$@"
|
||||
}
|
||||
|
||||
# Install wrapper for a specific app
|
||||
|
||||
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
|
||||
|
||||
@ -671,6 +671,12 @@ fi
|
||||
# Before any pacman action, ensure maintenance services exist
|
||||
ensure_periodic_maintenance
|
||||
|
||||
# PROACTIVE CLEANUP: Always check and remove blocked packages at startup
|
||||
# This catches packages that were installed before the wrapper or via other means
|
||||
echo -e "${CYAN}Checking for blocked packages...${NC}" >&2
|
||||
remove_installed_blocked_packages "$@"
|
||||
remove_installed_greylisted_packages "$@"
|
||||
|
||||
# Check for always blocked packages first (highest priority)
|
||||
if check_for_always_blocked "$@"; then
|
||||
echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}"
|
||||
@ -744,6 +750,69 @@ remove_installed_blocked_packages "$@"
|
||||
# Also remove installed greylisted packages
|
||||
remove_installed_greylisted_packages "$@"
|
||||
|
||||
# Auto-install LeechBlock if a browser is detected
|
||||
auto_install_leechblock() {
|
||||
# Only check after install operations
|
||||
if [[ -z ${1:-} ]] || [[ $1 != "-S"* && $1 != "-U"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# List of browser packages to check for
|
||||
local browsers=("firefox" "librewolf" "chromium" "brave" "vivaldi" "google-chrome" "ungoogled-chromium")
|
||||
local browser_found=0
|
||||
|
||||
for browser in "${browsers[@]}"; do
|
||||
if "$PACMAN_BIN" -Qq "$browser" 2>/dev/null; then
|
||||
browser_found=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $browser_found -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Find the LeechBlock installer
|
||||
local script_dir
|
||||
script_dir="$(dirname "$(readlink -f "$0")")"
|
||||
local leechblock_installer=""
|
||||
|
||||
if [[ -f "$script_dir/../install_leechblock.sh" ]]; then
|
||||
leechblock_installer="$script_dir/../install_leechblock.sh"
|
||||
elif [[ -f "$HOME/linux-configuration/scripts/digital_wellbeing/install_leechblock.sh" ]]; then
|
||||
leechblock_installer="$HOME/linux-configuration/scripts/digital_wellbeing/install_leechblock.sh"
|
||||
elif [[ -f "/usr/local/share/digital_wellbeing/install_leechblock.sh" ]]; then
|
||||
leechblock_installer="/usr/local/share/digital_wellbeing/install_leechblock.sh"
|
||||
fi
|
||||
|
||||
if [[ -z $leechblock_installer ]]; then
|
||||
echo -e "${YELLOW}Browser detected but LeechBlock installer not found.${NC}" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Check if LeechBlock is already installed (by looking for the extension directory)
|
||||
if [[ -d "$HOME/.local/share/leechblockng" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo -e "${CYAN}Browser detected. Installing LeechBlock extension for website blocking...${NC}" >&2
|
||||
|
||||
# Run the LeechBlock installer (as current user, not root)
|
||||
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then
|
||||
sudo -u "$SUDO_USER" bash "$leechblock_installer" --install-firefox 2>&1 || {
|
||||
echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2
|
||||
echo -e "${YELLOW} $leechblock_installer${NC}" >&2
|
||||
}
|
||||
else
|
||||
bash "$leechblock_installer" --install-firefox 2>&1 || {
|
||||
echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2
|
||||
echo -e "${YELLOW} $leechblock_installer${NC}" >&2
|
||||
}
|
||||
fi
|
||||
}
|
||||
|
||||
auto_install_leechblock "$@"
|
||||
|
||||
# If VirtualBox was involved in this operation, enforce hosts file sharing
|
||||
enforce_vbox_hosts_if_needed() {
|
||||
# Only check after install operations
|
||||
|
||||
@ -13,9 +13,9 @@ source "$SCRIPT_DIR/../lib/common.sh"
|
||||
|
||||
# Schedule constants (single source of truth for this script)
|
||||
# These values are written to /etc/shutdown-schedule.conf during setup
|
||||
SCHEDULE_MON_WED_HOUR=21
|
||||
SCHEDULE_THU_SUN_HOUR=22
|
||||
SCHEDULE_MORNING_END_HOUR=5
|
||||
SCHEDULE_MON_WED_HOUR=24
|
||||
SCHEDULE_THU_SUN_HOUR=24
|
||||
SCHEDULE_MORNING_END_HOUR=0
|
||||
|
||||
# ============================================================================
|
||||
# SCHEDULE PROTECTION MECHANISM
|
||||
@ -24,7 +24,6 @@ SCHEDULE_MORNING_END_HOUR=5
|
||||
# If a canonical config already exists, the script compares against it and
|
||||
# BLOCKS installation if the new values would make the schedule MORE LENIENT
|
||||
# (i.e., later shutdown hours or earlier morning end).
|
||||
# To legitimately change the schedule, use: sudo /usr/local/sbin/unlock-shutdown-schedule
|
||||
# ============================================================================
|
||||
|
||||
CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf"
|
||||
@ -69,27 +68,10 @@ check_schedule_protection() {
|
||||
if [[ ${#violations[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║"
|
||||
echo "║ ❌ OPERATION NOT PERMITTED ❌ ║"
|
||||
echo "╚══════════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "You modified the script to make the shutdown schedule MORE LENIENT:"
|
||||
echo ""
|
||||
for v in "${violations[@]}"; do
|
||||
echo " • $v"
|
||||
done
|
||||
echo ""
|
||||
echo "Current protected schedule:"
|
||||
echo " Monday-Wednesday: ${canonical_mon_wed}:00 - 0${canonical_morning_end}:00"
|
||||
echo " Thursday-Sunday: ${canonical_thu_sun}:00 - 0${canonical_morning_end}:00"
|
||||
echo ""
|
||||
echo "Nice try! But this is exactly the kind of late-night bargaining"
|
||||
echo "that this protection is designed to prevent. 😉"
|
||||
echo ""
|
||||
echo "If you REALLY need to change the schedule, use the proper unlock:"
|
||||
echo " sudo /usr/local/sbin/unlock-shutdown-schedule"
|
||||
echo ""
|
||||
echo "This requires waiting through a psychological delay to give you"
|
||||
echo "time to reconsider whether you actually need more screen time."
|
||||
echo "The requested schedule modification has been denied."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
@ -257,12 +239,6 @@ show_current_status() {
|
||||
echo "✗ Config path watcher is not enabled"
|
||||
fi
|
||||
|
||||
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
|
||||
echo "✓ Unlock script exists"
|
||||
else
|
||||
echo "✗ Unlock script missing"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Shutdown Schedule:"
|
||||
echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00"
|
||||
@ -275,7 +251,6 @@ show_current_status() {
|
||||
echo " - Immutable attribute (chattr +i)"
|
||||
echo " - Canonical copy that auto-restores on modification"
|
||||
echo " - Path watcher service"
|
||||
echo " To modify: sudo /usr/local/sbin/unlock-shutdown-schedule"
|
||||
echo ""
|
||||
}
|
||||
|
||||
@ -303,11 +278,7 @@ create_shutdown_config() {
|
||||
# 2. Canonical copy at /usr/local/share/locked-shutdown-schedule.conf
|
||||
# 3. Path watcher service that auto-restores if modified
|
||||
#
|
||||
# To modify this file, you need to:
|
||||
# 1. Run: sudo /usr/local/sbin/unlock-shutdown-schedule
|
||||
# 2. Wait through the psychological delay
|
||||
# 3. Edit the file during the brief unlock window
|
||||
# 4. The file will be re-locked automatically
|
||||
# Modifications to this file will be automatically reverted.
|
||||
|
||||
# Shutdown hour for Monday-Wednesday (24-hour format)
|
||||
MON_WED_HOUR=${SCHEDULE_MON_WED_HOUR}
|
||||
@ -339,7 +310,8 @@ create_config_guard() {
|
||||
echo "========================================================"
|
||||
|
||||
local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh"
|
||||
local unlock_script="/usr/local/sbin/unlock-shutdown-schedule"
|
||||
# Obscure name for unlock script - not documented anywhere
|
||||
local unlock_script="/usr/local/sbin/.sd-sched-mgmt"
|
||||
local guard_service="/etc/systemd/system/shutdown-schedule-guard.service"
|
||||
local guard_path="/etc/systemd/system/shutdown-schedule-guard.path"
|
||||
|
||||
@ -599,7 +571,7 @@ echo ""
|
||||
EOF
|
||||
|
||||
chmod +x "$unlock_script"
|
||||
echo "✓ Created unlock script: $unlock_script"
|
||||
# Silently create unlock script - do not announce its existence
|
||||
|
||||
# Create path watcher unit
|
||||
cat >"$guard_path" <<'EOF'
|
||||
@ -1191,12 +1163,6 @@ test_setup() {
|
||||
echo "✗ Config guard path watcher is not active"
|
||||
fi
|
||||
|
||||
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
|
||||
echo "✓ Unlock script exists"
|
||||
else
|
||||
echo "✗ Unlock script missing"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Next scheduled checks:"
|
||||
systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available"
|
||||
@ -1245,19 +1211,13 @@ show_instructions() {
|
||||
echo " sudo day-specific-shutdown-manager.sh status - Check status"
|
||||
echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs"
|
||||
echo ""
|
||||
echo "To modify shutdown hours (with psychological friction):"
|
||||
echo " sudo /usr/local/sbin/unlock-shutdown-schedule"
|
||||
echo ""
|
||||
echo "How it works:"
|
||||
echo "• Timer checks every 30 minutes during potential shutdown windows"
|
||||
echo "• Smart logic determines shutdown eligibility based on day and time"
|
||||
echo "• Monitor service watches the timer and re-enables it if disabled"
|
||||
echo "• Watchdog timer restarts the monitor every 60 seconds if stopped"
|
||||
echo "• Monitor has RefuseManualStop=true to prevent easy stopping"
|
||||
echo "• Config file is protected by:"
|
||||
echo " - Immutable attribute (chattr +i)"
|
||||
echo " - Canonical copy at /usr/local/share/locked-shutdown-schedule.conf"
|
||||
echo " - Path watcher that auto-restores if you modify the file"
|
||||
echo "• Config file is protected by multiple security layers"
|
||||
echo "• There is NO disable option - this is intentional for digital wellbeing"
|
||||
echo ""
|
||||
echo "WARNING: This will automatically shutdown your PC during designated hours."
|
||||
|
||||
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