joomla_check/checkjoomla.py

329 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2017-05-27 12:19:40 +02:00
import argparse
import hashlib
import os
import os.path
import re
2016-12-21 20:37:59 +01:00
import shutil
import stat
import sys
2017-05-27 12:19:40 +02:00
import socket
import urllib.request
import urllib.error
import zipfile
from distutils.version import StrictVersion
2017-05-27 12:19:40 +02:00
from pathlib import Path
##############################################################
2017-05-27 12:19:40 +02:00
# CONFIG
##############################################################
# BASE BASE
base_path = '/var/www/vhosts'
# CSV FILENAME
csv_file_name = 'joomla_status.csv'
2016-12-21 20:37:59 +01:00
# TMP DOWNLOAD DIR
tmp_dl_dir = '/tmp'
2017-05-27 12:19:40 +02:00
#############################################################
2017-05-27 12:19:40 +02:00
def get_joomla_version(version_file_path):
version_file = open(version_file_path, "r", -1, None, 'replace')
main_version = -1
dev_version = -1
2017-05-27 12:19:40 +02:00
for line in version_file:
if main_version != -1 and dev_version != -1:
break
2017-05-27 12:19:40 +02:00
version_match = re.search("""RELEASE[\s]*=[\s]*'?"?([0-9.]+)'?"?""", line)
if version_match is not None:
# print(version_match.group(1))
main_version = version_match.group(1)
version_match = re.search("""DEV_LEVEL[\s]*=[\s]*'?"?([0-9]+)'?"?""", line)
if version_match is not None:
# print(version_match.group(1))
dev_version = version_match.group(1)
version_file.close()
if main_version != -1 and dev_version != -1:
return str(main_version + "." + dev_version)
else:
return ""
2017-05-27 12:19:40 +02:00
def check_version(current_version, latest_version):
if StrictVersion(latest_version) > StrictVersion(current_version):
return False
else:
return True
2017-05-27 12:19:40 +02:00
def get_newest_version():
response = urllib.request.urlopen("http://update.joomla.org/core/list.xml")
highest_version = "0.0"
for line in response:
2017-05-27 12:19:40 +02:00
version_match = re.search("""[\s]version="?'?([0-9.]+)"?'?""", str(line))
if version_match is not None:
if StrictVersion(version_match.group(1)) > StrictVersion(highest_version):
highest_version = version_match.group(1)
return highest_version
2017-05-27 12:19:40 +02:00
def download_joomla_version(joomla_version):
version_match = re.search("""(\d+)\.(\d+)(\.(\d+))?([ab](\d+))?""", joomla_version)
2016-12-21 20:37:59 +01:00
dst_path = ""
if version_match is not None:
if version_match.group(1) == "2":
2016-12-21 20:37:59 +01:00
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...
2017-05-27 12:19:40 +02:00
dst_path = tmp_dl_dir + "/orig_joomla_" + joomla_version + ".zip"
2016-12-21 20:37:59 +01:00
dst_file = Path(dst_path)
if not dst_file.is_file():
2017-05-27 12:19:40 +02:00
if StrictVersion(joomla_version) > StrictVersion('3.7.1'):
2017-07-11 01:01:22 +02:00
version_string2 = ".".join(version_string.rsplit('-', 2))
2017-05-27 12:19:40 +02:00
url = "https://downloads.joomla.org/cms/" + \
version_path + "/" + version_string + \
"/Joomla_" + version_string2 + \
"-Stable-Full_Package.zip?format=zip"
else:
url = "https://downloads.joomla.org/cms/" + \
version_path + "/" + version_string + \
"/joomla_" + version_string + \
"-stable-full_package-zip?format=zip"
try:
2017-05-27 12:19:40 +02:00
urllib.request.urlretrieve(url, dst_path)
except (urllib.error.URLError, urllib.error.HTTPError, socket.error):
# print("Download of", url, "failed!")
dst_path = ""
2016-12-21 20:37:59 +01:00
return dst_path
2017-05-27 12:19:40 +02:00
def extract_downloaded_joomla_version(joomla_version, zip_file_path):
dst_path = tmp_dl_dir + "/orig_joomla_" + joomla_version
2016-12-21 20:37:59 +01:00
# extract a fresh copy...
shutil.rmtree(dst_path, onerror=remove_readonly)
try:
2017-05-27 12:19:40 +02:00
with zipfile.ZipFile(zip_file_path, "r") as zip_ref:
zip_ref.extractall(dst_path)
2017-05-27 12:19:40 +02:00
except (urllib.error.URLError, urllib.error.HTTPError, socket.error):
dst_path = ""
2016-12-21 20:37:59 +01:00
return dst_path
2017-05-27 12:19:40 +02:00
2017-05-27 12:56:35 +02:00
def remove_readonly(*arguments):
func, path, _ = arguments
2016-12-21 20:37:59 +01:00
if os.path.isdir(path):
os.chmod(path, stat.S_IWRITE)
func(path)
2017-05-27 12:19:40 +02:00
2016-12-21 20:37:59 +01:00
def get_dir_md5(dir_root):
exclude_dirs = {"installation", "tmp"}
2017-05-27 12:19:40 +02:00
md5_hash = hashlib.md5()
2017-05-27 12:56:35 +02:00
for dir_path, dir_names, file_names in os.walk(dir_root, topdown=True):
2016-12-21 20:37:59 +01:00
2017-05-27 12:56:35 +02:00
dir_names.sort(key=os.path.normcase)
file_names.sort(key=os.path.normcase)
2016-12-21 20:37:59 +01:00
2017-05-27 12:56:35 +02:00
dir_names[:] = [d for d in dir_names if d not in exclude_dirs]
2016-12-21 20:37:59 +01:00
2017-05-27 12:56:35 +02:00
for filename in file_names:
core_file_path = os.path.join(dir_path, filename)
2016-12-21 20:37:59 +01:00
# If some metadata is required, add it to the checksum
# 1) filename (good idea)
2017-05-27 12:56:35 +02:00
# hash.update(os.path.normcase(os.path.relpath(core_file_path, dir_root))
2016-12-21 20:37:59 +01:00
# 2) mtime (possibly a bad idea)
2017-05-27 12:56:35 +02:00
# st = os.stat(core_file_path)
2016-12-21 20:37:59 +01:00
# hash.update(struct.pack('d', st.st_mtime))
# 3) size (good idea perhaps)
# hash.update(bytes(st.st_size))
2017-05-27 12:56:35 +02:00
f = open(core_file_path, 'rb')
2016-12-21 20:37:59 +01:00
for chunk in iter(lambda: f.read(65536), b''):
2017-05-27 12:19:40 +02:00
md5_hash.update(chunk)
return md5_hash.hexdigest()
2016-12-21 20:37:59 +01:00
def cmp_joomla_directories(original_root, installation_root):
exclude_dirs = {"installation", "tmp", "logs"}
2017-05-27 12:19:40 +02:00
check_failures = []
2016-12-21 20:37:59 +01:00
2017-05-27 12:19:40 +02:00
for dir_path, dir_names, file_names in os.walk(original_root, topdown=True):
2016-12-21 20:37:59 +01:00
2017-05-27 12:19:40 +02:00
dir_names.sort(key=os.path.normcase)
file_names.sort(key=os.path.normcase)
2016-12-21 20:37:59 +01:00
2017-05-27 12:19:40 +02:00
dir_names[:] = [d for d in dir_names if d not in exclude_dirs]
for filename in file_names:
relative_path = os.path.relpath(dir_path, original_root)
2016-12-21 20:37:59 +01:00
if relative_path == ".":
relative_path = ""
2017-05-27 12:19:40 +02:00
orig_file_path = os.path.join(dir_path, filename)
inst_file_path = os.path.join(installation_root, os.path.join(relative_path, filename))
if os.path.isfile(inst_file_path):
2016-12-21 20:37:59 +01:00
hash_orig = hashlib.md5()
2017-05-27 12:19:40 +02:00
f = open(orig_file_path, 'rb')
2016-12-21 20:37:59 +01:00
for chunk in iter(lambda: f.read(65536), b''):
hash_orig.update(chunk)
f.close()
2017-05-27 12:19:40 +02:00
2016-12-21 20:37:59 +01:00
hash_inst = hashlib.md5()
2017-05-27 12:19:40 +02:00
f = open(inst_file_path, 'rb')
2016-12-21 20:37:59 +01:00
for chunk in iter(lambda: f.read(65536), b''):
hash_inst.update(chunk)
f.close()
if hash_orig.hexdigest() == hash_inst.hexdigest():
2017-05-27 12:19:40 +02:00
"""
print("file ok",
os.path.join(relative_path, filename),
hash_orig.hexdigest(),
hash_inst.hexdigest())
"""
2016-12-21 20:37:59 +01:00
pass
else:
2017-05-27 12:19:40 +02:00
"""
print("file NOT OK!",
os.path.join(relative_path, filename),
hash_orig.hexdigest(),
hash_inst.hexdigest())
"""
2016-12-21 20:37:59 +01:00
check_failures.append(os.path.join(relative_path, filename))
else:
2017-05-27 12:19:40 +02:00
# print("File", os.path.join(relative_path, filename), "is missing!")
2016-12-21 20:37:59 +01:00
pass
return check_failures
2017-05-27 12:19:40 +02:00
class ConsoleColors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
2017-05-27 12:19:40 +02:00
# MAIN
###########
2016-12-21 20:37:59 +01:00
parser = argparse.ArgumentParser(description='Check joomla installation state.')
parser.add_argument('-v', '--verbose', action='store_true', help='Print verbose output')
parser.add_argument('-n', '--nointegrity', action='store_true', help='Skip integrity check')
2016-12-21 20:37:59 +01:00
args = parser.parse_args()
newest_version = get_newest_version()
2017-05-27 12:19:40 +02:00
print(ConsoleColors.HEADER + "Newest Joomla Version: ", ConsoleColors.OKBLUE, newest_version, ConsoleColors.ENDC, "\n")
fobj = open(csv_file_name, "w")
2016-12-21 20:37:59 +01:00
fobj.write("Status;Integrity;Actual Version;Newest Version;Domain;Path\n")
2018-03-10 12:41:16 +01:00
for file_path in Path(base_path).glob('**/[vV]ersion.php'):
newest_version = get_newest_version()
version = get_joomla_version(str(file_path))
domain = "unknown"
match = re.search("""/([a-zA-Z0-9-]+\.[a-zA-Z]{2,})/""", str(file_path))
if match is not None:
domain = match.group(1)
if version:
2016-12-21 20:37:59 +01:00
version_status = "UNKN"
integrity_status = "UNKN"
if not check_version(version, newest_version):
2017-05-27 12:19:40 +02:00
print(ConsoleColors.FAIL, "[WARNING]", ConsoleColors.ENDC, "Outdated Joomla version found!\t[",
ConsoleColors.FAIL + version + ConsoleColors.ENDC, "] [",
ConsoleColors.WARNING + domain + ConsoleColors.ENDC, "] \tin ",
file_path)
2016-12-21 20:37:59 +01:00
version_status = "WARN"
else:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.OKGREEN, "[OK] ", ConsoleColors.ENDC, "Up to date Joomla version found!\t[",
ConsoleColors.OKGREEN + version + ConsoleColors.ENDC, "] [",
ConsoleColors.WARNING + domain + ConsoleColors.ENDC, "] \tin ",
file_path)
2016-12-21 20:37:59 +01:00
version_status = "OKOK"
if not args.nointegrity:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.HEADER, " -> Checking file integrity: ", ConsoleColors.ENDC, end=" ")
sys.stdout.flush()
dl_path = download_joomla_version(version)
2016-12-21 20:37:59 +01:00
if not dl_path:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.FAIL, "Failed to download joomla source!", ConsoleColors.ENDC)
2016-12-21 20:37:59 +01:00
else:
orig_root = extract_downloaded_joomla_version(version, dl_path)
2017-05-27 12:19:40 +02:00
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
2016-12-21 20:37:59 +01:00
if not orig_root:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.FAIL, "Failed to extract joomla source!", ConsoleColors.ENDC)
2016-12-21 20:37:59 +01:00
else:
result_list = cmp_joomla_directories(orig_root, cms_root)
2016-12-21 20:37:59 +01:00
if len(result_list) == 0:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.OKGREEN, "OK", ConsoleColors.ENDC)
integrity_status = "OKOK"
2016-12-21 20:37:59 +01:00
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:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.WARNING, "OK", ConsoleColors.ENDC, "Use -v to get details!")
integrity_status = "WARN"
else:
2017-05-27 12:19:40 +02:00
print(ConsoleColors.FAIL, "FAIL", ConsoleColors.ENDC, "Use -v to get details!")
integrity_status = "FAIL"
2017-05-27 12:19:40 +02:00
if args.verbose:
if len(result_list) > 0:
print('\tMissmatch: %s' % '\n\tMissmatch: '.join(map(str, result_list)))
2017-05-27 12:19:40 +02:00
fobj.write(
version_status + ";" + integrity_status + ";" + version + ";" + newest_version + ";" + domain + ";" + str(
file_path) + "\n")
print("") # empty last line
fobj.close()
2017-05-27 12:19:40 +02:00
print("\n" + ConsoleColors.HEADER + "All versions written to: ",
ConsoleColors.OKBLUE, csv_file_name, ConsoleColors.ENDC)