mirror of
https://github.com/ItsDrike/dotfiles.git
synced 2024-12-26 13:14:35 +00:00
Add custom system-wide scripts
This commit is contained in:
parent
9ce557ba05
commit
62c4bec0a7
81
root/usr/local/bin/auto-chroot
Executable file
81
root/usr/local/bin/auto-chroot
Executable file
|
@ -0,0 +1,81 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
yes_no() {
|
||||||
|
while true; do
|
||||||
|
printf "$1 (y/n): "
|
||||||
|
read -r yn
|
||||||
|
case $yn in
|
||||||
|
[Yy]* ) return 0;;
|
||||||
|
[Nn]* ) return 1;;
|
||||||
|
* ) echo "Please answer yes or no";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure we run as root
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
echo "Must be ran as root"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Take NEWROOT as 1st argument
|
||||||
|
if [ $# -ge 1 ]; then
|
||||||
|
NEWROOT="$1"
|
||||||
|
else
|
||||||
|
echo "Provide newroot directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Take chroot user as 2nd argument, default to root
|
||||||
|
if [ $# -ge 2 ]; then
|
||||||
|
USERNAME="$2"
|
||||||
|
else
|
||||||
|
USERNAME="root"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if given NEWROOT is already mounted, if it is
|
||||||
|
# set REMOUNT to the mount source, so that we can remount
|
||||||
|
# it once we're done.
|
||||||
|
df_out=$(df --output=source,target | grep -w "$NEWROOT")
|
||||||
|
if [ -n "$df_out" ]; then
|
||||||
|
REMOUNT="$(echo $df_out | awk '{print $1}')"
|
||||||
|
else
|
||||||
|
# If the target isn't mounted already, check
|
||||||
|
# if user gave $3 (mount location)
|
||||||
|
if [ $# -ge 3 ]; then
|
||||||
|
mount "$3" "$NEWROOT"
|
||||||
|
else
|
||||||
|
# If user didn't give mount location, try to
|
||||||
|
# mount according to fstab
|
||||||
|
if [ -n "$(grep -w "$NEWROOT" /etc/fstab)" ]; then
|
||||||
|
mount "$NEWROOT"
|
||||||
|
else
|
||||||
|
# Ask for user confirmation to ensure that filesystem
|
||||||
|
# is ready for chroot in given NEWROOT, exit if not
|
||||||
|
yes_no "$NEWROOT wasn't mounted, is your filesystem in place?" || exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mount necessary directories for chroot to be possible
|
||||||
|
mount --types proc /proc "$NEWROOT/proc"
|
||||||
|
mount --rbind /sys "$NEWROOT/sys"
|
||||||
|
mount --make-rslave "$NEWROOT/sys"
|
||||||
|
mount --rbind /dev "$NEWROOT/dev"
|
||||||
|
mount --make-rslave "$NEWROOT/dev"
|
||||||
|
|
||||||
|
# Use /bin/su for chrooting with --login to also run
|
||||||
|
# /etc/profile and ~/.profile or ~/.zprofile
|
||||||
|
chroot "$NEWROOT" "/bin/su" "$USERNAME" --login
|
||||||
|
|
||||||
|
# Unmount recursively mounted directories
|
||||||
|
umount -l "$NEWROOT/dev"
|
||||||
|
umount -l "$NEWROOT/sys"
|
||||||
|
umount -l "$NEWROOT/proc"
|
||||||
|
umount -R "$NEWROOT"
|
||||||
|
|
||||||
|
# Remount partition according to fstab if REMOUT is set
|
||||||
|
# in order to leave the filesystem in the state it was
|
||||||
|
if [ -n "$REMOUNT" ]; then
|
||||||
|
mount "$REMOUNT" "$NEWROOT"
|
||||||
|
fi
|
11
root/usr/local/bin/cron-notify
Executable file
11
root/usr/local/bin/cron-notify
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Crontab requires DISPLAY and XDG_RUNTIME_HOME
|
||||||
|
# to be set when running notify-send, this script
|
||||||
|
# makes defines those to make it eaiser to send
|
||||||
|
# notifications from crontab without cluttering it
|
||||||
|
# It sets "Cron Notification" title, rest of the
|
||||||
|
# arguments are passed to notfiy-send
|
||||||
|
|
||||||
|
XDG_RUNTIME_DIR="/run/user/$(id -u)" \
|
||||||
|
DISPLAY=:0 \
|
||||||
|
notify-send "$@"
|
37
root/usr/local/bin/incremental-backup
Executable file
37
root/usr/local/bin/incremental-backup
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to perform incremental backups using rsync
|
||||||
|
# It is often ran as crontab rule for automated backup solution
|
||||||
|
#
|
||||||
|
# This script will respect .rsync-filter files, which can be used
|
||||||
|
# to define custom exclude rules for files/dirs in which it is present
|
||||||
|
|
||||||
|
if [ $# -lt 2 ]; then
|
||||||
|
echo "Invalid amount of arguments passed!"
|
||||||
|
echo "Arguments: [Source path] [Backup path]"
|
||||||
|
echo " Source path: directory to be backed up, usually '/'"
|
||||||
|
echo " Backup path: directory to back up to (destination), usually mounted drive"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
SOURCE_DIR="$1"
|
||||||
|
BACKUP_DIR="$2"
|
||||||
|
DATETIME="$(date '+%Y-%m-%d_%H:%M:%S')"
|
||||||
|
BACKUP_PATH="${BACKUP_DIR}/${DATETIME}"
|
||||||
|
LATEST_LINK="${BACKUP_DIR}/latest"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
rsync -avHAXS \
|
||||||
|
--delete \
|
||||||
|
--filter='dir-merge /.rsync-filter' \
|
||||||
|
--link-dest "${LATEST_LINK}" \
|
||||||
|
"${@:3}" "${SOURCE_DIR}/" "${BACKUP_PATH}"
|
||||||
|
|
||||||
|
# Only attempt to override the symlink if we made new backup_path
|
||||||
|
# user might've passed --dry-run option in which case we wouldn't
|
||||||
|
# want to override latest symlink to non-existent location
|
||||||
|
if [ -d "${BACKUP_PATH}" ]; then
|
||||||
|
rm "${LATEST_LINK}" 2>/dev/null
|
||||||
|
ln -s "${BACKUP_PATH}" "${LATEST_LINK}"
|
||||||
|
fi
|
292
root/usr/local/bin/tamper-check
Executable file
292
root/usr/local/bin/tamper-check
Executable file
|
@ -0,0 +1,292 @@
|
||||||
|
#!/bin/python3
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import colorama
|
||||||
|
except ImportError:
|
||||||
|
from unittest.mock import Mock
|
||||||
|
class NoReprMock(Mock):
|
||||||
|
__repr__ = lambda self: ""
|
||||||
|
colorama = NoReprMock()
|
||||||
|
|
||||||
|
colorama.init(autoreset=True)
|
||||||
|
|
||||||
|
|
||||||
|
# default path to the JSON file that stores known file checksums
|
||||||
|
# this can be overridden by using `--checksum-file=path` flag
|
||||||
|
CHECKSUM_FILE = Path('/usr/local/share/tamper-check/checksums.json')
|
||||||
|
|
||||||
|
|
||||||
|
def _yes_no(text: str, add_yn: bool = True) -> bool:
|
||||||
|
if add_yn:
|
||||||
|
text += ' (y/n): '
|
||||||
|
while True:
|
||||||
|
user_inp = input(text).lower()
|
||||||
|
if user_inp in ('y', 'yes'):
|
||||||
|
return True
|
||||||
|
elif user_inp in ('n', 'no'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_checksum_dict(checksum_file: Path) -> dict[Path, str]:
|
||||||
|
"""Read the JSON checksum file and return it as python dictionary object."""
|
||||||
|
try:
|
||||||
|
with open(checksum_file, 'r') as f:
|
||||||
|
checksums = json.load(f)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.YELLOW}Checksum file not found: {colorama.Fore.RESET}'
|
||||||
|
f"'{colorama.Fore.BLUE}{checksum_file}{colorama.Fore.RESET}'{colorama.Fore.YELLOW} "
|
||||||
|
'Creating new empty checksum file...'
|
||||||
|
)
|
||||||
|
checksum_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
checksums = {}
|
||||||
|
with open(checksum_file, 'w') as f:
|
||||||
|
json.dump(checksums, f, indent=4)
|
||||||
|
return checksums
|
||||||
|
except PermissionError:
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.RED}PermissionError: {colorama.Fore.RESET}'
|
||||||
|
'to run tamper-check you must have read access to checksum file: '
|
||||||
|
f"'{colorama.Fore.BLUE}{checksum_file}{colorama.Fore.RESET}' (forgot sudo?)"
|
||||||
|
)
|
||||||
|
exit(2)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.RED}Checksum file is corrupted, unable to decode JSON. '
|
||||||
|
f"{colorama.Fore.RESET}('{colorama.Fore.BLUE}{checksum_file}{colorama.Fore.RESET}').\n"
|
||||||
|
f'Error text: {e}'
|
||||||
|
)
|
||||||
|
exit(3)
|
||||||
|
else:
|
||||||
|
dct = {}
|
||||||
|
for file_str, checksum in checksums.items():
|
||||||
|
dct[Path(file_str)] = checksum
|
||||||
|
return dct
|
||||||
|
|
||||||
|
|
||||||
|
def _get_checksum(file: Path) -> str:
|
||||||
|
"""Obtain a checksum of given file"""
|
||||||
|
proc = subprocess.run(['sha256sum', file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||||
|
proc_stdout = proc.stdout.decode('utf-8')
|
||||||
|
|
||||||
|
if "No such file or directory" in proc_stdout:
|
||||||
|
raise FileNotFoundError(f"'{file}' not found, can't produce sha256 checksum")
|
||||||
|
elif "Permission denied" in proc_stdout:
|
||||||
|
raise PermissionError(f"Unable to read file '{file}'")
|
||||||
|
elif "Is a directory" in proc_stdout:
|
||||||
|
raise RuntimeError(f"{file} is a directory, can't produce sha256sum")
|
||||||
|
|
||||||
|
return proc_stdout.replace(f' {file}\n', '')
|
||||||
|
|
||||||
|
|
||||||
|
def _update_checksum(file_path: Path, checksum: str, checksum_file: Path, new_entry: bool = False) -> None:
|
||||||
|
"""Update existing checksums or add new file entries in checksum_file"""
|
||||||
|
checksums = _get_checksum_dict(checksum_file)
|
||||||
|
|
||||||
|
if new_entry and file_path in checksums:
|
||||||
|
print(
|
||||||
|
f"{colorama.Fore.RED}Path {colorama.Fore.RESET}"
|
||||||
|
f"'{colorama.Fore.BLUE}{file_path}{colorama.Fore.RESET}' {colorama.Fore.RED}"
|
||||||
|
"is already in the checksum file perhaps you wanted `--update`?"
|
||||||
|
)
|
||||||
|
raise SystemExit(3)
|
||||||
|
|
||||||
|
checksums[file_path] = checksum
|
||||||
|
|
||||||
|
writeable_checksums = {str(file_path): file_checksum for file_path, file_checksum in checksums.items()}
|
||||||
|
try:
|
||||||
|
with open(checksum_file, 'w') as f:
|
||||||
|
json.dump(writeable_checksums, f, indent=4)
|
||||||
|
except PermissionError:
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.RED}PermissionError: {colorama.Fore.RESET}'
|
||||||
|
'To add a new rule, you must have write access to: '
|
||||||
|
f"'{colorama.Fore.BLUE}{checksum_file}{colorama.Fore.RESET}' (forgot sudo?)"
|
||||||
|
)
|
||||||
|
raise SystemExit(2)
|
||||||
|
|
||||||
|
|
||||||
|
def update(file_path: Path, checksum_file: Path, text: str, no_confirm: bool = False) -> bool:
|
||||||
|
"""Ask user if a file should be updated, or update automatically if no_confirm is True"""
|
||||||
|
new_checksum = _get_checksum(file_path)
|
||||||
|
|
||||||
|
if no_confirm:
|
||||||
|
print(text + ' checksum auto-updating')
|
||||||
|
elif not _yes_no(text + ' update checksum?'):
|
||||||
|
print(f'{colorama.Fore.RED} -> Staying mismatched')
|
||||||
|
return False
|
||||||
|
|
||||||
|
_update_checksum(file_path, new_checksum, checksum_file)
|
||||||
|
print(f'{colorama.Fore.GREEN} -> Updated')
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def run_check(checksum_file: Path, verbose: bool) -> list[Path]:
|
||||||
|
"""
|
||||||
|
Go through all files listed in checksum_file and make sure that the checksums are matching.
|
||||||
|
Return all entries which didn't match.
|
||||||
|
"""
|
||||||
|
checksums = _get_checksum_dict(checksum_file)
|
||||||
|
|
||||||
|
not_matched = []
|
||||||
|
for file, stored_checksum in checksums.items():
|
||||||
|
line = f"Checksum of '{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}': "
|
||||||
|
|
||||||
|
try:
|
||||||
|
real_sha256_sum = _get_checksum(file)
|
||||||
|
except PermissionError as exc:
|
||||||
|
print(line + f'{colorama.Fore.YELLOW}SKIPPED [PermissionError - no read perms]')
|
||||||
|
if verbose:
|
||||||
|
print(f' -> Error text: {colorama.Fore.CYAN}{exc}')
|
||||||
|
continue
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
print(line + f'{colorama.Fore.YELLOW}FAILED [FileNotFound - fix checksum file]')
|
||||||
|
if verbose:
|
||||||
|
print(f' -> Error text: {colorama.Fore.CYAN}{exc}')
|
||||||
|
continue
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(line + f'{colorama.Fore.YELLOW}FAILED [{exc.__class__.__name__}: {exc} - fix checksum file]')
|
||||||
|
if verbose:
|
||||||
|
print(f' -> Error text: {colorama.Fore.CYAN}{exc}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
if real_sha256_sum == stored_checksum:
|
||||||
|
print(line + f'{colorama.Fore.GREEN}OK')
|
||||||
|
else:
|
||||||
|
not_matched.append(file)
|
||||||
|
print(line + f'{colorama.Fore.RED}FAIL [Checksum Mismatch]')
|
||||||
|
if verbose:
|
||||||
|
print(f' -> detected: {colorama.Fore.CYAN}{real_sha256_sum}')
|
||||||
|
print(f' -> stored: {colorama.Fore.CYAN}{stored_checksum}')
|
||||||
|
|
||||||
|
return not_matched
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(*, checksum_file_default) -> dict:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='tamper-check is a command line utility to automate checking for file edits. '
|
||||||
|
'This is achieved by storing sha256 checksums of each added file and comparing them.'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-v', '--verbose', action='store_true',
|
||||||
|
help='Verbose mode, show checksums on failures and some more info'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-u', '--update', action='store_true',
|
||||||
|
help='If invalid checksum is found, ask user if it should be updated (y/n)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--no-confirm', action='store_true',
|
||||||
|
help='Used in combination with `--update`, automatically assumes `y` for all questions'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--checksum-file', metavar='FILE', type=Path, default=checksum_file_default,
|
||||||
|
help='JSON file storing the file checksums'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-a', '--add', metavar='FILE', nargs='+', action='extend', type=Path, default=[],
|
||||||
|
dest='files_to_add', help='Add a new file to the list of check entries'
|
||||||
|
)
|
||||||
|
|
||||||
|
namespace = parser.parse_args()
|
||||||
|
cli_args = {k: v for k, v in vars(namespace).items()}
|
||||||
|
|
||||||
|
# Handle non-existing paths
|
||||||
|
for path in cli_args['files_to_add']:
|
||||||
|
if not path.exists():
|
||||||
|
raise FileNotFoundError(path)
|
||||||
|
if not path.is_file():
|
||||||
|
raise RuntimeError("Can't add a directory")
|
||||||
|
if not cli_args['checksum_file'].exists():
|
||||||
|
raise FileNotFoundError(cli_args['checksum_file'])
|
||||||
|
|
||||||
|
return cli_args
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Run the program as intended, return the exit code"""
|
||||||
|
try:
|
||||||
|
run_parameters = parse_args(checksum_file_default=CHECKSUM_FILE)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
path = exc.args[0]
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.RED}FileNotFoundError: {colorama.Fore.RESET}'
|
||||||
|
f"'{colorama.Fore.BLUE}{path}{colorama.Fore.RESET}' -> invalid path"
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
except RuntimeError as exc:
|
||||||
|
print(
|
||||||
|
f'{colorama.Fore.RED}{exc.__class__.__name__}: {colorama.Fore.RESET}'
|
||||||
|
f"'{colorama.Fore.BLUE}{exc}{colorama.Fore.RESET}'"
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
except SystemExit as exc:
|
||||||
|
return exc.code
|
||||||
|
|
||||||
|
if len(run_parameters["files_to_add"]) > 0:
|
||||||
|
for file_to_add in run_parameters["files_to_add"]:
|
||||||
|
checksum = _get_checksum(file_to_add)
|
||||||
|
try:
|
||||||
|
_update_checksum(
|
||||||
|
file_to_add, checksum,
|
||||||
|
run_parameters["checksum_file"],
|
||||||
|
new_entry=True
|
||||||
|
)
|
||||||
|
except SystemExit as e:
|
||||||
|
return e.code
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Added '{colorama.Fore.BLUE}{file_to_add}{colorama.Fore.RESET}': "
|
||||||
|
f"'{colorama.Fore.CYAN}{checksum}{colorama.Fore.RESET}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
return 0 # don't proceed to check if we're adding files
|
||||||
|
|
||||||
|
# Run the check
|
||||||
|
mismatched_files = run_check(run_parameters["checksum_file"], run_parameters["verbose"])
|
||||||
|
if len(mismatched_files) == 0:
|
||||||
|
return 0 # all files are ok
|
||||||
|
|
||||||
|
print("\nFiles with mismatched checksums:")
|
||||||
|
prefix = f"{colorama.Fore.RED} - {colorama.Fore.RESET}"
|
||||||
|
unfixed = []
|
||||||
|
for mismatched_file in mismatched_files:
|
||||||
|
line = prefix + f"'{colorama.Fore.BLUE}{mismatched_file}{colorama.Fore.RESET}'"
|
||||||
|
|
||||||
|
if not run_parameters["update"]:
|
||||||
|
unfixed.append(mismatched_file)
|
||||||
|
print(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not update(
|
||||||
|
file_path=mismatched_file,
|
||||||
|
checksum_file=run_parameters["checksum_file"],
|
||||||
|
no_confirm=run_parameters["no_confirm"],
|
||||||
|
text=line
|
||||||
|
):
|
||||||
|
unfixed.append(mismatched_file)
|
||||||
|
|
||||||
|
if len(unfixed) > 0:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f'\n{colorama.Fore.GREEN}All checksums are correct')
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit_code = main()
|
||||||
|
try:
|
||||||
|
exit(exit_code) # exit gracefully, with silent exit code
|
||||||
|
except TypeError:
|
||||||
|
# Some python interpreters/extensions (such as IPython) don't like exit.
|
||||||
|
# sys.exit will raise a full exception and go to python traceback, exiting
|
||||||
|
# with code 1. The real exit code will be preserved in the traceback.
|
||||||
|
# This isn't ideal, but it's better than out of the place TypeError
|
||||||
|
# and with exit code 0, this will exit normally.
|
||||||
|
# CPython doesn't do this and most users will never experience this.
|
||||||
|
sys.exit(exit_code)
|
Loading…
Reference in a new issue