diff --git a/checkjoomla.py b/checkjoomla.py index 2a7cc7b..592c87e 100644 --- a/checkjoomla.py +++ b/checkjoomla.py @@ -3,6 +3,15 @@ from pathlib import Path import re import urllib.request +from urllib.request import urlretrieve +import zipfile +import shutil +import stat +import os +import os.path +import hashlib +import sys +import argparse from distutils.version import StrictVersion ############################################################## @@ -15,6 +24,9 @@ base_path = '/var/www/vhosts' # CSV FILENAME csv_file_name = 'joomla_status.csv' +# TMP DOWNLOAD DIR +tmp_dl_dir = '/tmp' + ############################################################# def get_joomla_version(filepath): @@ -58,6 +70,120 @@ def get_newest_version(): return highest_version +def download_joomla_version(version): + version_match = re.search("""(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?""", version) + + dst_path = "" + if version_match is not None: + if version_match.group(1) == 2: + version_path = "joomla25" + else: + version_path = "joomla3" + version_string = version_match.group(1) + "-" + version_match.group(2) + "-" + version_match.group(4) + + # check if file has already been downloaded... + dst_path = tmp_dl_dir + "/" + version + ".zip" + dst_file = Path(dst_path) + if not dst_file.is_file(): + url = "https://downloads.joomla.org/cms/" + version_path + "/" + version_string + "/joomla_" + version_string + "-stable-full_package-zip?format=zip" + urllib.request.urlretrieve (url, dst_path) + + return dst_path + +def extract_downloaded_joomla_version(version, path): + dst_path = tmp_dl_dir + "/" + version + + # extract a fresh copy... + shutil.rmtree(dst_path, onerror=remove_readonly) + + with zipfile.ZipFile(path, "r") as zip_ref: + zip_ref.extractall(dst_path) + + return dst_path + +def remove_readonly(func, path, excinfo): + if os.path.isdir(path): + os.chmod(path, stat.S_IWRITE) + func(path) + +def get_dir_md5(dir_root): + exclude_dirs = {"installation", "tmp"} + + hash = hashlib.md5() + for dirpath, dirnames, filenames in os.walk(dir_root, topdown=True): + + dirnames.sort(key=os.path.normcase) + filenames.sort(key=os.path.normcase) + + dirnames[:] = [d for d in dirnames if d not in exclude_dirs] + + for filename in filenames: + filepath = os.path.join(dirpath, filename) + + # If some metadata is required, add it to the checksum + + # 1) filename (good idea) + # hash.update(os.path.normcase(os.path.relpath(filepath, dir_root)) + + # 2) mtime (possibly a bad idea) + # st = os.stat(filepath) + # hash.update(struct.pack('d', st.st_mtime)) + + # 3) size (good idea perhaps) + # hash.update(bytes(st.st_size)) + + f = open(filepath, 'rb') + for chunk in iter(lambda: f.read(65536), b''): + hash.update(chunk) + + return hash.hexdigest() + +def cmp_joomla_directories(original_root, installation_root): + exclude_dirs = {"installation", "tmp", "logs"} + + check_failures = [] + + for dirpath, dirnames, filenames in os.walk(original_root, topdown=True): + + dirnames.sort(key=os.path.normcase) + filenames.sort(key=os.path.normcase) + + dirnames[:] = [d for d in dirnames if d not in exclude_dirs] + + for filename in filenames: + relative_path = os.path.relpath(dirpath, original_root) + + if relative_path == ".": + relative_path = "" + + orig_filepath = os.path.join(dirpath, filename) + inst_filepath = os.path.join(installation_root, os.path.join(relative_path, filename)) + + if os.path.isfile(inst_filepath): + hash_orig = hashlib.md5() + f = open(orig_filepath, 'rb') + for chunk in iter(lambda: f.read(65536), b''): + hash_orig.update(chunk) + f.close() + + hash_inst = hashlib.md5() + f = open(inst_filepath, 'rb') + for chunk in iter(lambda: f.read(65536), b''): + hash_inst.update(chunk) + f.close() + + if hash_orig.hexdigest() == hash_inst.hexdigest(): + #print("file ok", os.path.join(relative_path, filename), hash_orig.hexdigest(), hash_inst.hexdigest()) + pass + else: + #print("file NOT OK!!!!!!!!!!!!!!!!!!!!!!!", os.path.join(relative_path, filename), hash_orig.hexdigest(), hash_inst.hexdigest()) + check_failures.append(os.path.join(relative_path, filename)) + else: + #print("File", os.path.join(relative_path, filename), "is missing!") + pass + + return check_failures + class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' @@ -71,12 +197,17 @@ class bcolors: # MAIN ########### +parser = argparse.ArgumentParser(description='Check joomla installation state.') +parser.add_argument('-v', '--verbose', action='store_true', help='Print verbose output') + +args = parser.parse_args() + newest_version = get_newest_version() print(bcolors.HEADER + "Newest Joomla Version: ", bcolors.OKBLUE, newest_version, bcolors.ENDC, "\n") fobj = open(csv_file_name, "w") -fobj.write("Status;Actual Version;Newest Version;Domain;Path\n") +fobj.write("Status;Integrity;Actual Version;Newest Version;Domain;Path\n") for file_path in Path(base_path).glob('**/version.php'): newest_version = get_newest_version() @@ -88,12 +219,55 @@ for file_path in Path(base_path).glob('**/version.php'): domain = match.group(1) if version: + version_status = "UNKN" + integrity_status = "UNKN" if not check_version(version, newest_version): print(bcolors.FAIL, "[WARNING]", bcolors.ENDC, "Outdated Joomla version found!\t[", bcolors.FAIL + version + bcolors.ENDC, "] [", bcolors.WARNING + domain + bcolors.ENDC, "] \tin ", file_path) - fobj.write("WARN;" + version + ";" + newest_version + ";" + domain + ";" + str(file_path) + "\n") + version_status = "WARN" else: print(bcolors.OKGREEN, "[OK] ", bcolors.ENDC, "Up to date Joomla version found!\t[", bcolors.OKGREEN + version + bcolors.ENDC, "] [", bcolors.WARNING + domain + bcolors.ENDC, "] \tin ", file_path) - fobj.write("OKOK;" + version + ";" + newest_version + ";" + domain + ";" + str(file_path) + "\n") + version_status = "OKOK" + + print(bcolors.HEADER, " -> Checking file integrity: ", bcolors.ENDC, end=" ") + sys.stdout.flush() + dl_path = download_joomla_version(version) + + if not dl_path: + print(bcolors.FAIL, "Failed to download joomla source!", bcolors.ENDC) + else: + orig_root = extract_downloaded_joomla_version(version, dl_path) + cms_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(str(file_path))))) # strip "libraries/cms/version/version.php" from filename + + if not orig_root: + print(bcolors.FAIL, "Failed to extract joomla source!", bcolors.ENDC) + else: + result_list = cmp_joomla_directories(orig_root, cms_root) + + if len(result_list) == 0: + print(bcolors.OKGREEN, "OK", bcolors.ENDC) + integrity_status = "OKOK" + else: + # check if only image files differ... if so ignore it. + real_fail_count = 0 + + for fail_path in result_list: + if fail_path.lower().endswith(".jpg") or fail_path.lower().endswith(".png"): + pass + else: + real_fail_count = real_fail_count + 1 + + if real_fail_count == 0: + print(bcolors.WARNING, "OK", bcolors.ENDC, "Use -v to get details!") + integrity_status = "WARN" + else: + print(bcolors.FAIL, "FAIL", bcolors.ENDC, "Use -v to get details!") + integrity_status = "FAIL" + + if args.verbose: + print('\tMissmatch: %s' % '\n\tMissmatch: '.join(map(str, result_list))) + + fobj.write(version_status + ";" + integrity_status + ";" + version + ";" + newest_version + ";" + domain + ";" + str(file_path) + "\n") + print("") # empty last line fobj.close() print("\n" + bcolors.HEADER + "All versions written to: ", bcolors.OKBLUE, csv_file_name, bcolors.ENDC)