mirror of
https://github.com/ItsDrike/dotfiles.git
synced 2024-12-27 21:54:34 +00:00
255 lines
9.4 KiB
Python
Executable file
255 lines
9.4 KiB
Python
Executable file
#!/bin/python3
|
|
# TODO: Add color highlighting to help
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
import colorama
|
|
import json
|
|
|
|
colorama.init(autoreset=True)
|
|
|
|
|
|
# default verbosity state, controlled with `--verbose` flag
|
|
VERBOSE = False
|
|
# update ability is turned off by default, controlled with
|
|
# `--update` flag. This is off to avoid need for user input
|
|
# and only work with returned error codes, making it possible
|
|
# to run this script easily with cronjobs or similar schedulers
|
|
ENABLE_UPDATE = False
|
|
# update automatically without asking the user first (will only
|
|
# have effect if ENABLE_UPDATE is True - or turned by user), this
|
|
# will still inform user about those files being updated, but user
|
|
# won't be able to choose not to update
|
|
AUTO_UPDATE = False
|
|
# default path to the JSON file that stores known file checksums
|
|
# this can be overrided by using `--checksum-file=path` flag
|
|
CHECKSUM_FILE = '/usr/local/share/tamper-check/checksums.json'
|
|
# This shouldn't be touched, entries in this list should be valid
|
|
# path strings which are the new files that should be added to
|
|
# CHECKSUM_FILE, controlled by flag `--add=/path/to/file`
|
|
TO_ADD = []
|
|
|
|
|
|
def _print_help(prepend_newline=False):
|
|
if prepend_newline:
|
|
print()
|
|
print(
|
|
'tamper-check is a command line utility to automate checking for file edits.\n'
|
|
'This is achieved by storing sha256 checksums of each added file and comparing them\n\n'
|
|
'Accepted flags:\n'
|
|
' `-u`/`--update`: If invalid checksum is found, ask user if it should be updated (y/n)\n'
|
|
' `-a=path`/`--add=path`: Add a new file to the list of check entries\n'
|
|
' `--no-confirm`: Used in combination with `--update`, automatically assumes `y` for all questions\n'
|
|
' `--auto-update`: Combines `--update` and `--no-confirm`\n'
|
|
' `-f=path/--checksum-file=path: JSON file storing the file checksums, defaults to /usr/local/share/tamper-check/checksums.json\n`'
|
|
' `-v`/`--verbose`: Verbose mode, show checksums on failures and some more info\n'
|
|
' `-h`/`--help`: Show this help'
|
|
)
|
|
|
|
|
|
def _yes_no(text, add_yn=True):
|
|
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(file):
|
|
try:
|
|
with open(file, 'r') as f:
|
|
checksums = json.load(f)
|
|
return checksums
|
|
except FileNotFoundError:
|
|
print(
|
|
f'{colorama.Fore.YELLOW}Checksum file not found: {colorama.Fore.RESET}'
|
|
f"'{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}'{colorama.Fore.YELLOW} "
|
|
'Creating new empty checksum file...'
|
|
)
|
|
os.makedirs(os.path.dirname(file), exist_ok=True)
|
|
checksums = {}
|
|
with open(file, 'w') as f:
|
|
json.dump(checksums, f)
|
|
return checksums
|
|
|
|
|
|
|
|
def _add_file(file_path, checksum, checksum_file, update_only=False):
|
|
checksums = _get_checksum_dict(checksum_file)
|
|
|
|
if file_path in checksums and not update_only:
|
|
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`?"
|
|
)
|
|
return False
|
|
|
|
checksums[file_path] = checksum
|
|
try:
|
|
with open(checksum_file, 'w') as f:
|
|
json.dump(checksums, f)
|
|
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?)"
|
|
)
|
|
exit(2)
|
|
|
|
return True
|
|
|
|
|
|
def _get_checksum(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:
|
|
print(
|
|
f'{colorama.Fore.RED}FileNotFound: {colorama.Fore.RESET}'
|
|
f"'{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}' not found, can't produce "
|
|
"sha256 checksum, check the checksum file and remove this entry"
|
|
)
|
|
exit(2)
|
|
elif "Permission denied" in proc_stdout:
|
|
raise PermissionError(f"PermissionError: Unable to read file '{file}'")
|
|
return proc_stdout.replace(f' {file}\n', '')
|
|
|
|
|
|
def _command_exists(command):
|
|
proc = subprocess.run(f'which {command}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
|
|
return proc.returncode == 0
|
|
|
|
|
|
def ask_update(file_path, checksum_file):
|
|
new_checksum = _get_checksum(file_path)
|
|
|
|
file_line = f"'{colorama.Fore.BLUE}{file_path}{colorama.Fore.RESET}'"
|
|
if AUTO_UPDATE or _yes_no(' - ' + file_line + ' update checksum?'):
|
|
if AUTO_UPDATE:
|
|
print(' - updating checksum for ' + file_line) # path won't get printed by _yes_no(), so print here
|
|
result = _add_file(file_path, new_checksum, checksum_file, update_only=True)
|
|
print(f'{colorama.Fore.GREEN} -> Updated')
|
|
return result
|
|
|
|
print(f'{colorama.Fore.RED} -> Staying mismatched')
|
|
return False
|
|
|
|
|
|
def run_check():
|
|
checksums = _get_checksum_dict(CHECKSUM_FILE)
|
|
not_matched = []
|
|
for file, checksum in checksums.items():
|
|
try:
|
|
sha256_sum = _get_checksum(file)
|
|
except PermissionError:
|
|
print(
|
|
f"Checksum of '{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}': "
|
|
f'{colorama.Fore.YELLOW}SKIPPED [PermissionError - no read perms]')
|
|
continue
|
|
|
|
if sha256_sum != checksum:
|
|
not_matched.append(file)
|
|
print(
|
|
f"Checksum of '{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}': "
|
|
f'{colorama.Fore.RED}FAIL [Checksum Mismatch]'
|
|
)
|
|
if VERBOSE:
|
|
print(f" -> detected: {colorama.Fore.CYAN}{sha256_sum}")
|
|
print(f" -> stored: {colorama.Fore.CYAN}{checksum}")
|
|
else:
|
|
print(f"Checksum of '{colorama.Fore.BLUE}{file}{colorama.Fore.RESET}': {colorama.Fore.GREEN}OK")
|
|
return not_matched
|
|
|
|
|
|
def analyze_args():
|
|
# Using globals isn't usually ideal solution,
|
|
# but it is the easiest way to handle this
|
|
global VERBOSE, ENABLE_UPDATE, AUTO_UPDATE, TO_ADD, CHECKSUM_FILE
|
|
|
|
try:
|
|
args = sys.argv[1:]
|
|
except IndexError:
|
|
return
|
|
|
|
for arg in args:
|
|
if arg in ('-u', '--update'):
|
|
ENABLE_UPDATE = True
|
|
elif arg == '--no-confirm':
|
|
AUTO_UPDATE = True
|
|
elif arg == '--auto-update':
|
|
ENABLE_UPDATE = True
|
|
AUTO_UPDATE = True
|
|
elif arg.startswith('--add=') or arg.startswith('-a='):
|
|
path = arg.replace('--add=', '').replace('-a=', '')
|
|
if os.path.exists(path):
|
|
path = os.path.abspath(path)
|
|
TO_ADD.append(path)
|
|
else:
|
|
print(
|
|
f'{colorama.Fore.RED}FileNotFoundError: {colorama.Fore.RESET}'
|
|
f"'{colorama.Fore.BLUE}{path}{colorama.Fore.RESET}' -> invalid path"
|
|
)
|
|
exit(2)
|
|
elif arg in ('-v', '--verbose'):
|
|
VERBOSE = True
|
|
elif arg.startswith('-f=') or arg.startswith('--checksum-file='):
|
|
path = arg.replace('--checksum-file=', '').replace('-f=', '')
|
|
if os.path.exists(path):
|
|
path = os.path.abspath(path)
|
|
CHECKSUM_FILE = path
|
|
else:
|
|
print(
|
|
f'{colorama.Fore.RED}FileNotFoundError: {colorama.Fore.RESET}'
|
|
f"'{colorama.Fore.BLUE}{path}{colorama.Fore.RESET}' -> invalid path"
|
|
)
|
|
elif arg in ('-h', '--help'):
|
|
_print_help()
|
|
exit()
|
|
else:
|
|
print(f'{colorama.Fore.RED}Unrecognized flag: {colorama.Fore.YELLOW}{arg}')
|
|
_print_help(prepend_newline=True)
|
|
exit(2)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
analyze_args()
|
|
|
|
# if file adding was requested, handle it
|
|
if len(TO_ADD) > 0:
|
|
for file in TO_ADD:
|
|
checksum = _get_checksum(file)
|
|
if _add_file(file, checksum, CHECKSUM_FILE):
|
|
print(
|
|
f"Added '{colorama.Fore.YELLOW}{file}{colorama.Fore.RESET}': "
|
|
f"'{colorama.Fore.CYAN}{checksum}{colorama.Fore.RESET}'"
|
|
)
|
|
exit(0) # when adding files, don't run the check too
|
|
|
|
# run the actual checksum verifier
|
|
not_matched = run_check()
|
|
|
|
if len(not_matched) > 0 and ENABLE_UPDATE is True:
|
|
print('\nFiles with mismatched checksums:')
|
|
unfixed = not_matched.copy()
|
|
for mismatched_file in not_matched:
|
|
if ask_update(mismatched_file, CHECKSUM_FILE) is True:
|
|
# User did not choose to update the checksum
|
|
unfixed.remove(mismatched_file)
|
|
if len(unfixed) > 0:
|
|
exit(1) # we still have some unfixed mismatched checksums, exit with 1
|
|
|
|
elif len(not_matched) > 0:
|
|
print('\nFiles with mismatched checksums:')
|
|
for mismatched_file in not_matched:
|
|
print(
|
|
f'{colorama.Fore.RED} - '
|
|
f"{colorama.Fore.RESET}'{colorama.Fore.BLUE}{mismatched_file}{colorama.Fore.RESET}'"
|
|
)
|
|
exit(1) # exit with error code in case we have changed checksums
|
|
else:
|
|
print(f'\n{colorama.Fore.GREEN}All checksums are correct')
|