2021-05-14 21:10:18 +00:00
|
|
|
#!/bin/python3
|
|
|
|
|
|
|
|
|
|
|
|
# Dict of file paths with their sha256 checksums
|
|
|
|
# make sure to run with `--auto-update` flag if you're running first time on this machine
|
|
|
|
# to automatically update all stored checksums to new values
|
|
|
|
files = {
|
|
|
|
'/etc/pam.d/system-auth': '89d62406b2d623a76d53c33aca98ce8ee124ed4a450ff6c8a44cfccca78baa2f',
|
2021-05-14 22:27:44 +00:00
|
|
|
'/etc/pam.d/su': '7d8962b4a2cd10cf4bc13da8949a4a6151b572d39e87b7125be55f882b16c4da',
|
2021-05-14 21:10:18 +00:00
|
|
|
'/etc/pam.d/sudo': 'd1738818070684a5d2c9b26224906aad69a4fea77aabd960fc2675aee2df1fa2',
|
2021-05-14 23:26:38 +00:00
|
|
|
'/etc/passwd': '28d6bec52ac5b4957a2c30dfcd15008dc1a39665c27abce97408489f3dbf02c9',
|
|
|
|
'/etc/shadow': 'a24f72cba4cbc6b0a8433da2f4b011f31345068e3e5d6bebed6fb6a35769bd59',
|
|
|
|
'/etc/ssh/sshd_config': '515db2484625122b425447f7e673649e3d89b57577eaa29395017676735907b',
|
2021-05-14 21:10:18 +00:00
|
|
|
'/bin/sudo': '0ffaf9e93a080ca1698837729641c283d24500d6cdd2cb4eb8e42427566a230e',
|
|
|
|
'/bin/su': '3101438405d98e71e9eb68fbc5a33536f1ad0dad5a1c8aacd6da6c95ef082194',
|
2021-05-14 21:44:54 +00:00
|
|
|
'/usr/bin/passwd': 'd4df1659159737bb4c08a430d493d257d75cdd93e18427946265ae5862a714c7',
|
|
|
|
'/usr/bin/chsh': '6bc0ae69620dde18f7942e2573afb4a6200b10269612151f48f54ef8423a64fe',
|
|
|
|
'/usr/bin/chfn': '63178af1347a62f58874640d38d605d3cb1bebe8092533787965ba317e8b553b',
|
2021-05-14 21:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
# default state of ENABLE_UPDATE variable, this should be kept to false
|
|
|
|
# to make sure this script can run as cronjob, not just manually by user
|
|
|
|
# this variable will get set to True if the script is ran with `--update`
|
|
|
|
ENABLE_UPDATE = False
|
|
|
|
# default state of AUTO_UPDATE variable, this has no effect if above is
|
|
|
|
# False, if not and this variable is True, user input is skipped entirely
|
|
|
|
# and the checksums are updated automatically, otherwise user confirmation
|
|
|
|
# is needed, controlled with `--no-confirm`, or `--auto-update`
|
|
|
|
AUTO_UPDATE = False
|
|
|
|
# default state of TO_ADD variable, this controls new files, checksums of
|
|
|
|
# which should be added to the `files` dictionary, this is controlled
|
|
|
|
# by flag `--add=/path/to/file`
|
|
|
|
TO_ADD = []
|
|
|
|
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
# - CODE PART, DON'T EDIT UNLESS YOU KNOW WHAT YOU'RE DOING -
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
import subprocess
|
|
|
|
import sys
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
def _print_help(prepend_newline=False):
|
|
|
|
if prepend_newline:
|
|
|
|
print()
|
|
|
|
print(
|
|
|
|
'Accepted flags:\n'
|
2021-05-14 23:26:38 +00:00
|
|
|
' `-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'
|
2021-05-14 21:10:18 +00:00
|
|
|
' `--no-confirm`: Used in combination with `--update`, automatically assumes `y` for all questions\n'
|
|
|
|
' `--auto-update`: Combines `--update` and `--no-confirm`\n'
|
2021-05-14 23:26:38 +00:00
|
|
|
' `-e`/`--edit`: Edit this file using your $EDITOR (falls back to vi)\n'
|
2021-05-14 21:10:18 +00:00
|
|
|
' `-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 _add_file(file_path, checksum):
|
|
|
|
this = os.path.abspath(__file__)
|
|
|
|
pattern = re.compile(r"files = \{\n(\s+)(['\"])(.+\n)+\}")
|
|
|
|
|
|
|
|
if file_path in files:
|
|
|
|
print(
|
|
|
|
f"Unable to add '{file_path}', this file is already in the file dictionary, "
|
|
|
|
"perhaps you wanted `--update`?"
|
|
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
|
|
with open(this, 'r') as f:
|
|
|
|
contents = f.read()
|
|
|
|
try:
|
|
|
|
match = list(pattern.finditer(contents))[0]
|
|
|
|
except IndexError:
|
|
|
|
raise RuntimeError("Unable to detect files dict with regex, changed dict structure?")
|
|
|
|
|
|
|
|
new_line = f"{match[1]}{match[2]}{file_path}{match[2]}: {match[2]}{checksum}{match[2]},\n"
|
|
|
|
add_position = match.end() - 1 # before bracket symbol
|
|
|
|
if not match[3].endswith(',\n'):
|
|
|
|
add_position -= 1 # before newline character on non-comma line
|
|
|
|
contents = contents[:add_position] + ',' + contents[add_position:]
|
|
|
|
add_position += 2
|
|
|
|
|
|
|
|
new_contents = contents[:add_position] + new_line + contents[add_position:]
|
2021-05-14 22:27:44 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
with open(this, 'w') as f:
|
|
|
|
f.write(new_contents)
|
|
|
|
except PermissionError:
|
|
|
|
print(f"PermissionError: To add a new rule, you must have write access to: '{this}' (forgot sudo?)")
|
|
|
|
exit(2)
|
2021-05-14 21:10:18 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _update_file(file_path, new_checksum, stored_checksum):
|
|
|
|
this = os.path.abspath(__file__)
|
|
|
|
pattern = re.compile(rf"(\s+)(['\"]){file_path}['\"]:(\s+)['\"]{stored_checksum}['\"],?")
|
|
|
|
with open(this, 'r') as f:
|
|
|
|
contents = f.read()
|
|
|
|
|
|
|
|
new_contents = re.sub(
|
|
|
|
pattern,
|
|
|
|
rf'\1\2{file_path}\2:\3\g<2>{new_checksum}\2,',
|
|
|
|
contents
|
|
|
|
)
|
|
|
|
|
|
|
|
if contents == new_contents: # Line wasn't find, perhaps it's a new file?
|
|
|
|
return False
|
|
|
|
|
2021-05-14 22:27:44 +00:00
|
|
|
try:
|
|
|
|
with open(this, 'w') as f:
|
|
|
|
f.write(new_contents)
|
|
|
|
except PermissionError as e:
|
|
|
|
print(f"PermissionError: To update a rule, you must have write access to: '{this}' (forgot sudo?)")
|
|
|
|
exit(2)
|
2021-05-14 21:10:18 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def ask_update(file_path, new_checksum, stored_checksum):
|
|
|
|
if not ENABLE_UPDATE:
|
|
|
|
return False # only proceed if user input is enabled
|
|
|
|
|
|
|
|
if AUTO_UPDATE or _yes_no('Do you wish to update this checksum?'):
|
|
|
|
result = _update_file(file_path, new_checksum, stored_checksum)
|
|
|
|
print(f"Updated '{file_path}' checksum entry\n")
|
|
|
|
return result
|
|
|
|
|
|
|
|
return False # return False if user didn't agree
|
|
|
|
|
|
|
|
|
|
|
|
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"File '{file}' not found, can't produce sha256 checksum, "
|
|
|
|
"check the 'files' dictionary on the top of the program and remove this entry."
|
|
|
|
)
|
|
|
|
exit(2)
|
|
|
|
return proc_stdout.replace(f' {file}\n', '')
|
|
|
|
|
|
|
|
|
2021-05-14 23:26:38 +00:00
|
|
|
def _command_exists(command):
|
|
|
|
proc = subprocess.run(f'which {command}', stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
|
|
|
|
return proc.returncode == 0
|
|
|
|
|
|
|
|
|
|
|
|
def run_editor():
|
|
|
|
try:
|
|
|
|
editor = os.environ['EDITOR']
|
|
|
|
except KeyError:
|
|
|
|
for candidate in ('nvim', 'vim', 'vi', 'emacs', 'nano', 'ne', 'tilde'):
|
|
|
|
if _command_exists(candidate):
|
|
|
|
editor = candidate
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
print('Unable to find editor software, set $EDITOR')
|
|
|
|
exit(2)
|
|
|
|
|
|
|
|
this = os.path.abspath(__file__)
|
|
|
|
cmd = f'{editor} {this}'
|
|
|
|
if not os.access(this, os.W_OK):
|
|
|
|
if _command_exists('sudo'):
|
|
|
|
cmd = 'sudo ' + cmd
|
|
|
|
elif _command_exists('doas'):
|
|
|
|
cmd = 'doas ' + cmd
|
|
|
|
return subprocess.run(cmd, shell=True)
|
|
|
|
|
|
|
|
|
2021-05-14 21:10:18 +00:00
|
|
|
def run_check():
|
|
|
|
not_matched = []
|
|
|
|
for file, checksum in files.items():
|
|
|
|
sha256_sum = _get_checksum(file)
|
|
|
|
if sha256_sum != checksum:
|
|
|
|
print(f"WARNING: {file} doesn't match the checksum")
|
|
|
|
print(f" -> detected: {sha256_sum}")
|
|
|
|
print(f" -> stored: {checksum}")
|
|
|
|
|
|
|
|
if ask_update(file, sha256_sum, checksum) is False:
|
|
|
|
# User did not choose to update the checksum
|
|
|
|
not_matched.append(file)
|
|
|
|
return not_matched
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_args():
|
|
|
|
# Using globals isn't usually ideal solution,
|
|
|
|
# but it is the easiest way to handle this
|
|
|
|
global ENABLE_UPDATE, AUTO_UPDATE, TO_ADD
|
|
|
|
|
|
|
|
try:
|
|
|
|
args = sys.argv[1:]
|
|
|
|
except IndexError:
|
|
|
|
return
|
|
|
|
|
|
|
|
for arg in args:
|
2021-05-14 23:26:38 +00:00
|
|
|
if arg in ('-u', '--update'):
|
2021-05-14 21:10:18 +00:00
|
|
|
ENABLE_UPDATE = True
|
|
|
|
elif arg == '--no-confirm':
|
|
|
|
AUTO_UPDATE = True
|
|
|
|
elif arg == '--auto-update':
|
|
|
|
ENABLE_UPDATE = True
|
|
|
|
AUTO_UPDATE = True
|
2021-05-14 23:26:38 +00:00
|
|
|
elif '--add=' in arg or '-a=' in arg:
|
|
|
|
path = arg.replace('--add=', '').replace('-a=', '')
|
2021-05-14 21:10:18 +00:00
|
|
|
if os.path.exists(path):
|
|
|
|
TO_ADD.append(path)
|
|
|
|
else:
|
|
|
|
print(f"Can't add {path} -> non-existent path")
|
|
|
|
exit(2)
|
2021-05-14 23:26:38 +00:00
|
|
|
elif arg in ('-e', '--edit'):
|
|
|
|
run_editor()
|
|
|
|
exit()
|
2021-05-14 21:10:18 +00:00
|
|
|
elif arg in ('-h', '--help'):
|
|
|
|
_print_help()
|
|
|
|
exit()
|
|
|
|
else:
|
|
|
|
print(f'Unrecognized flag: {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):
|
|
|
|
print(f"Added '{file}': '{checksum}'")
|
|
|
|
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:
|
|
|
|
exit(1) # exit with error code in case we have changed checksums
|
|
|
|
else:
|
|
|
|
print("All checksums are correct")
|