2016-12-21 14:01:55 +01:00
#!/usr/bin/env python3
from pathlib import Path
import re
import urllib . request
2016-12-21 20:37:59 +01:00
from urllib . request import urlretrieve
import zipfile
import shutil
import stat
import os
import os . path
import hashlib
import sys
import argparse
2016-12-21 14:01:55 +01:00
from distutils . version import StrictVersion
##############################################################
## 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 '
2016-12-21 14:01:55 +01:00
#############################################################
def get_joomla_version ( filepath ) :
fobj = open ( filepath , " r " , - 1 , None , ' replace ' )
version = - 1
dev_version = - 1
for line in fobj :
if version != - 1 and dev_version != - 1 :
break
match = re . search ( """ RELEASE[ \ s]*=[ \ s]* ' ? " ?([0-9 \ .]+) ' ? " ? """ , line )
if match is not None :
#print(match.group(1))
version = match . group ( 1 )
match = re . search ( """ DEV_LEVEL[ \ s]*=[ \ s]* ' ? " ?([0-9]+) ' ? " ? """ , line )
if match is not None :
#print(match.group(1))
dev_version = match . group ( 1 )
fobj . close ( )
if version != - 1 and dev_version != - 1 :
return str ( version + " . " + dev_version )
else :
return " "
def check_version ( version , newest_version ) :
if StrictVersion ( newest_version ) > StrictVersion ( version ) :
return False
else :
return True
def get_newest_version ( ) :
response = urllib . request . urlopen ( " http://update.joomla.org/core/list.xml " )
highest_version = " 0.0 "
for line in response :
match = re . search ( """ [ \ s]version= " ? ' ?([0-9 \ .]+) " ? ' ? """ , str ( line ) )
if match is not None :
if StrictVersion ( match . group ( 1 ) ) > StrictVersion ( highest_version ) :
highest_version = match . group ( 1 )
return highest_version
2016-12-21 20:37:59 +01:00
def download_joomla_version ( version ) :
version_match = re . search ( """ ( \ d+) \ .( \ d+)( \ .( \ d+))?([ab]( \ d+))? """ , version )
dst_path = " "
if version_match is not None :
2016-12-21 20:52:42 +01:00
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...
2016-12-21 20:52:42 +01:00
dst_path = tmp_dl_dir + " /orig_joomla_ " + version + " .zip "
2016-12-21 20:37:59 +01:00
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 "
2016-12-21 20:52:42 +01:00
try :
urllib . request . urlretrieve ( url , dst_path )
except :
dst_path = " "
2016-12-21 20:37:59 +01:00
return dst_path
def extract_downloaded_joomla_version ( version , path ) :
2016-12-21 20:52:42 +01:00
dst_path = tmp_dl_dir + " /orig_joomla_ " + version
2016-12-21 20:37:59 +01:00
# extract a fresh copy...
shutil . rmtree ( dst_path , onerror = remove_readonly )
2016-12-21 20:52:42 +01:00
try :
with zipfile . ZipFile ( path , " r " ) as zip_ref :
zip_ref . extractall ( dst_path )
except :
dst_path = " "
2016-12-21 20:37:59 +01:00
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
2016-12-21 14:01:55 +01:00
class bcolors :
HEADER = ' \033 [95m '
OKBLUE = ' \033 [94m '
OKGREEN = ' \033 [92m '
WARNING = ' \033 [93m '
FAIL = ' \033 [91m '
ENDC = ' \033 [0m '
BOLD = ' \033 [1m '
UNDERLINE = ' \033 [4m '
# 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 ' )
2016-12-21 20:52:42 +01:00
parser . add_argument ( ' -n ' , ' --nointegrity ' , action = ' store_true ' , help = ' Skip integrity check ' )
2016-12-21 20:37:59 +01:00
args = parser . parse_args ( )
2016-12-21 14:01:55 +01:00
newest_version = get_newest_version ( )
print ( bcolors . HEADER + " Newest Joomla Version: " , bcolors . OKBLUE , newest_version , bcolors . 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 " )
2016-12-21 14:01:55 +01:00
for file_path in Path ( base_path ) . glob ( ' **/version.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 "
2016-12-21 14:01:55 +01:00
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 , " ] \t in " , file_path )
2016-12-21 20:37:59 +01:00
version_status = " WARN "
2016-12-21 14:01:55 +01:00
else :
print ( bcolors . OKGREEN , " [OK] " , bcolors . ENDC , " Up to date Joomla version found! \t [ " , bcolors . OKGREEN + version + bcolors . ENDC , " ] [ " , bcolors . WARNING + domain + bcolors . ENDC , " ] \t in " , file_path )
2016-12-21 20:37:59 +01:00
version_status = " OKOK "
2016-12-21 20:52:42 +01:00
if not args . nointegrity :
print ( bcolors . HEADER , " -> Checking file integrity: " , bcolors . ENDC , end = " " )
sys . stdout . flush ( )
dl_path = download_joomla_version ( version )
2016-12-21 20:37:59 +01:00
2016-12-21 20:52:42 +01:00
if not dl_path :
print ( bcolors . FAIL , " Failed to download joomla source! " , bcolors . ENDC )
2016-12-21 20:37:59 +01:00
else :
2016-12-21 20:52:42 +01:00
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
2016-12-21 20:37:59 +01:00
2016-12-21 20:52:42 +01:00
if not orig_root :
print ( bcolors . FAIL , " Failed to extract joomla source! " , bcolors . ENDC )
2016-12-21 20:37:59 +01:00
else :
2016-12-21 20:52:42 +01:00
result_list = cmp_joomla_directories ( orig_root , cms_root )
2016-12-21 20:37:59 +01:00
2016-12-21 20:52:42 +01:00
if len ( result_list ) == 0 :
print ( bcolors . OKGREEN , " OK " , bcolors . ENDC )
integrity_status = " OKOK "
2016-12-21 20:37:59 +01:00
else :
2016-12-21 20:52:42 +01:00
# 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 :
if len ( result_list ) > 0 :
print ( ' \t Missmatch: %s ' % ' \n \t Missmatch: ' . join ( map ( str , result_list ) ) )
2016-12-21 20:37:59 +01:00
fobj . write ( version_status + " ; " + integrity_status + " ; " + version + " ; " + newest_version + " ; " + domain + " ; " + str ( file_path ) + " \n " )
print ( " " ) # empty last line
2016-12-21 14:01:55 +01:00
fobj . close ( )
print ( " \n " + bcolors . HEADER + " All versions written to: " , bcolors . OKBLUE , csv_file_name , bcolors . ENDC )