PHP Classes

File: sitepages_guard.php

Recommend this page to a friend!
  Classes of Alexander Selifonov   Site pages guard   sitepages_guard.php   Download  
File: sitepages_guard.php
Role: Class source
Content type: text/plain
Description: Main class module
Class: Site pages guard
Monitor and restore damaged application files
Author: By
Last change: small changes
Date: 14 years ago
Size: 22,624 bytes
 

Contents

Class file image Download
<?PHP /** * @name sitepages_guard.php * Class for saving info about all html/script pages on site, and monitoring their unexpected changes * (with auto-restoring feature) * created 18.09.2009 (dd.mm.yyyy) * modified 28.11.2009 * @version 1.01.003 * @Author Alexander Selifonov, <alex@selifan.ru> * @link http://www.selifan.ru * PHP required : 5.x * @license BSD - http://www.opensource.org/licenses/bsd-license.php **/ class CSitePagesGuard { const CHANGED_FILE = 1; const NEW_FILE = 2; const SUSPICIOUS_FILE = 3; # changed file that contains one or more "virus/malware signtures" strings, it's probably changed by malware const FILE_WAS_RESTORED = 0x10; const FILE_RESTORE_ERROR = 0x20; const RESTORE_NONE = 0; const RESTORE_ONLY_SUSPICIOUS = 1; const RESTORE_ALL_CHANGED = 2; private $_folders = array(); # all folders to be monitored private $_backupfolder = false; private $_datafile = ''; # filename for indexing results (site file names and hash-summs) private $_dwords_file = ''; private $_fileext = array('htm','html','php','inc','phtm','phtml','cgi','pl','asp','aspx'); # monitored files extensions private $_errormessage = ''; private $_dangerouswords = array(); private $finfo = array(); private $_email = ''; private $_stats = array(); private $_titles = array(); private $_found_signature = ''; private $_fullcheckmode = 0; # 0 = check file changes by size+modif.time (fast), 1 = by checking md5 sum for file (slow) public function CSitePagesGuard($rootfolder='./', $param=0) { global $as_iface; $this->_datafile = dirname(__FILE__).'/siteguard.filelist'; $this->_datafile = dirname(__FILE__).'/siteguard.vsignatures'; $this->_folders = is_array($rootfolder) ? $rootfolder : preg_split("/[\t,;|]+/",$rootfolder); if(is_array($param)) { if(isset($param['datafile'])) $this->_datafile = $param['datafile']; if(isset($param['backupfolder'])) $this->_backupfolder = $param['backupfolder']; if(isset($param['email'])) $this->_email = $param['email']; if(isset($param['extensions'])) { if(is_string($param['extensions'])) $this->_fileext = preg_split("/[\s,;|]+/", $param['extensions']); elseif(is_array($param['extensions'])) $this->_fileext = $param['extensions']; } if(isset($param['fullcheck'])) $this->_fullcheckmode = $param['fullcheck']; } # txtchanged $txtnew $txtsusp $txtrestored $txtrest_err $this->_titles['file_changed'] = isset($as_iface['spg_file_changed']) ? $as_iface['spg_file_changed'] : 'File changed'; $this->_titles['file_is_new'] = isset($as_iface['spg_file_is_new']) ? $as_iface['spg_file_is_new'] : 'New file'; $this->_titles['file_suspicious'] = isset($as_iface['spg_file_suspicious']) ? $as_iface['spg_file_suspicious'] : 'SUSPICIOUS file (dangerous signatures found)'; $this->_titles['file_restored'] = isset($as_iface['spg_file_restored']) ? $as_iface['spg_file_restored'] : ' was successfully restored'; $this->_titles['file_restore_err'] = isset($as_iface['spg_file_restore_err']) ? $as_iface['spg_file_restore_err'] : ' FILE RESTORING ERROR !'; $this->_titles['file_nochanges'] = isset($as_iface['spg_file_nochanges']) ? $as_iface['spg_file_nochanges'] : 'No changed or new files found'; $this->_titles['message_subj'] = isset($as_iface['spg_message_subj']) ? $as_iface['spg_message_subj'] : 'Report - changed files on Your site'; $this->_titles['write_error'] = isset($as_iface['err_write_tofile']) ? $as_iface['err_write_tofile'] : 'Writing to file error'; if(file_exists($this->_mwsig_file)) $this->__LoadMWSignatures(); # auto-load "virus" signatures } /** * Set array of "dangerous" words that are probably result of malware injection. * * @param mixed $param filename, or [,;|] delimited string, or an array with dangerous words (virus signatures) */ public function SetMalwareSignatures($param) { if(is_string($param)) { if(is_file($param)) { $this->__LoadMWSignatures($param); } else $this->_mwsignatures = preg_split("/[\t,;|]+/", $param); } elseif(is_array($param)) $this->_mwsignatures = $param; } /** * Adds file extension(s) to be monitored * * @param mixed $par string with new extension or an array with extension list * @param mixed $b_cleancurrent 1 or true to clean current extension list */ public function AddFileExtension($par,$b_cleancurrent=false) { if($b_cleancurrent) $this->_fileext[] = array(); if(is_string($par)) $par = preg_split("/[\s,;|]+/",$par); if(is_array($par)) $this->_fileext = array_merge($this->_fileext, $par); } private function __LoadMWSignatures($fname='') { if($fname) $this->_mwsig_file = $fname; $lines = @file($this->_mwsig_file); if(!$lines) echo ($this->_errormessage = 'error reading virus def.file '.$this->_mwsig_file); $this->_mwsignatures = array(); foreach($lines as $line) { $line = trim($line); if(strlen($line)<4) continue; $splt = preg_split("/[\t|]+/",$line); if(count($splt)>1) $this->_mwsignatures[$splt[0]] = $splt[1]; # assoc.presentation: virus name=>virus signature else $this->_mwsignatures[] = $line; } unset($lines); } /** * Registers (re-registers) info about ALL program/html files, and saves gzipped backup copies, if needed * @param $report_suspicious orders to check files before registering, and report if some "virus signatures" found * @returns string, summary report of registered files to be monitored */ public function RegisterAllFiles($report_suspicious=false) { global $as_iface; $ret_susp = ''; $this->_stats = array('sourcesize'=>0, 'gzipsize'=>0); if(!empty($this->_backupfolder)) { if(file_exists($this->_backupfolder)) { $this->__CleanBackupfolder(); } else { @mkdir($this->_backupfolder,077,true); # try resursive dir creating (PHP5 !) } } if(file_exists($this->_datafile) && !is_writable($this->_datafile)) { return ($this->_errormessage = $this->_titles['write_error'] . ' ' . $this->_datafile); } $filelist = array(); foreach($this->_folders as $onefolder) { $fl2 = $this->GetFilesInFolder($onefolder); $filelist = array_merge($filelist,$fl2); } $fout = fopen($this->_datafile,'w'); if(!is_resource($fout)) { return ($this->_errormessage = $this->_titles['write_error'] . ' ' . $this->_datafile); } foreach($filelist as $fname) { $this->_stats['sourcesize'] += ($fsize = filesize($fname)); $hash = ''; if($fsize >0) { $body = ''; $hash = @md5_file($fname); $filetime = filemtime($fname); if(($report_suspicious) && ($susp = $this->IsFileSuspicious($fname))) { $ret_susp .= $fname . ' - '.$this->_titles['file_suspicious'] . (is_string($susp)? " ($susp)":'') ."<br />\n"; } # save packed (gz) backup copy of the file, so it will be possible to auto-restore it if($hash!='' && !empty($this->_backupfolder) && function_exists('gzopen')) { #make gzipped copy of a file $gzipname = $this->_backupfolder."/$hash.gz"; if(!file_exists($gzipname)) { $this->__PackFile($fname,$hash); } $this->_stats['gzipsize'] += @filesize($gzipname); } } fwrite($fout,"$fname\t$fsize\t$filetime\t$hash\n"); } fclose($fout); $rettext = "Registered files : <b>".count($filelist). '</b> of summary size: <b>' . number_format($this->_stats['sourcesize']) . '</b>, gzipped size : <b>' . number_format($this->_stats['gzipsize']) ."</b><br />\n"; if($ret_susp) { $rettext .= '<hr />'.(isset($as_iface['spg_title_suspfound']) ? $as_iface['spg_title_suspfound'] : 'Attention, some suspisious files were found') . " :<br />\n$ret_susp"; } return $rettext; } /** * Refreshes info for new or updated files and saves updated info. Backup gzipped copies created if needed. * @returns integer count of new/updated files */ public function UpdateFilesInfo($report=false, $report_suspicious=false) { $refcount = 0; # refreshed files counter $changed = array(); $this->_stats = array('sourcesize'=>0, 'gzipsize'=>0); if(!file_exists($this->_datafile)) return $this->RegisterAllFiles(); $filelist = array(); foreach($this->_folders as $onefolder) { $fl2 = $this->GetFilesInFolder($onefolder); $filelist = array_merge($filelist, $fl2); } $this->__LoadFilesInfo(); $ret = ''; $delold = array(); foreach($filelist as $filename) { $md5 = md5_file($filename); $old_md5 = $this->finfo[$filename][2]; $ftime = filemtime($filename); $fsize = filesize($filename); if(!isset($this->finfo[$filename])) $refresh_type = self::NEW_FILE; else { $refresh_type = ($fsize!=$this->finfo[$filename][0] || $ftime!=$this->finfo[$filename][1] || $md5 !== $old_md5) ? self::CHANGED_FILE : 0; } if($refresh_type) { $b_susp = false; if($report_suspicious) { $b_susp = $this->IsFileSuspicious($filename); } $this->finfo[$filename] = array($fsize,$ftime, $md5); if($this->_backupfolder) $this->__PackFile($filename, $md5); $refcount++; $ftext = ($refresh_type==self::CHANGED_FILE) ? $this->_titles['file_changed'] : $this->_titles['file_is_new']; if($b_susp) $ftext .= ' ' . $this->_titles['file_suspicious'] . ' : '.$this->_found_signature; $ret .= "$filename : $ftext<br />"; if($refresh_type==self::CHANGED_FILE) $delold[$old_md5]=1; } } if($refcount) { $this->__SaveFilesInfo(); } if(count($delold)) { foreach($this->finfo as $fname =>$fdata) { if(isset($delold[$fdata[2]])) $delold[$fdata[2]] +=1; } foreach($delold as $md5 => $cnt) { # no more references to gz file, so delete it if($cnt<2) @unlink($this->_backupfolder."/$md5.gz"); } } return ($report)? $ret : $refcount; } private function __PackFile($fname, $md5name) { $last_modif = filemtime($fname); $gzipname = $this->_backupfolder.'/'.$md5name.'.gz'; $ret = false; if(($gzhandle = @gzopen($gzipname,'wb'))) { $fin = @fopen($fname,'rb'); if($fin) { while(!feof($fin)) { @gzwrite($gzhandle,fread($fin,4098)); } fclose($fin); $ret = true; } @gzclose($gzhandle); if($fin) @touch($gzipname,$last_modif); # saves file modification date/time } return $ret; # returns true if file was successfully gzipped to backup folder } /** * restores file from gzipped backup copy * * @param mixed $md5name "hash" filename in backup folder * @param mixed $destfname destination file path/name */ private function __UnpackFile($md5name, $destfname) { $gzipname = $this->_backupfolder . $md5name . '.gz'; if(!file_exists($gzipname)) { $this->_errormessage = 'gzipped file is absent: '.$gzipname; return false; } $gzreader = @gzopen($gzipname,'rb'); $ret = true; if(is_resource($gzreader)) { $hdest = @fopen($destfname,'wb'); if($hdest) { while(!gzeof($gzreader)) { $written=fwrite($hdest, gzread($gzreader,4096)); if($written===false) break; } fclose($hdest); } else { $ret = false; $this->_errormessage = 'open destination file for writing error: ' . $destfname; } gzclose($gzreader); } else { $ret = false; $this->_errormessage = 'open gzip error: ' . $gzipname; } if($ret) { @touch($destfname, filemtime($gzipname)); } return $ret; } /** * Restores all deleted files from backup copy * * @param boolean $report true to return verbose report, otherwise - restored files count * @returns text report or restored files count */ public function RestoreDeletedFiles($report=false) { $ret = ($report)? '' : 0; if(!is_array($this->finfo) || count($this->finfo)<1) $this->__LoadFilesInfo(); foreach($this->finfo as $fname=>$fparam) { if(!file_exists($fname)) { $result = $this->__UnpackFile($fparam[2],$fname); if($result) { if($report) $ret .= "$fname - {$this->_titles['file_restored']}<br />\n"; else $ret++; } else { if($report) $ret .= "$fname - {$this->_titles['write_error']}<br />\n"; else $ret++; } } } return $ret; } private function GetFilesInFolder($folder) { $fdata = array(); $allmasks = '*.' . implode(',*.', $this->_fileext); foreach (glob($folder . '{'.$allmasks.'}', GLOB_BRACE) as $filename){ if(is_file($filename)) { $fdata[] = $filename; } } $dirh = opendir($folder); while(is_resource($dirh) && ($dirname = readdir($dirh))) { if(is_dir($folder.$dirname)) { if($dirname==='.' || $dirname==='..') continue; $arr = $this->GetFilesInFolder($folder.$dirname.'/'); if(is_array($arr) && count($arr)>0) $fdata = array_merge($fdata, $arr); } } return $fdata; } private function __LoadFilesInfo() { $this->finfo = array(); $fread = @fopen($this->_datafile,'r'); if(!is_resource($fread)) { echo ($this->_errormessage = "Data file {$this->_datafile} does not exist or not readable"); return false; } while(!feof($fread)) { $line = @fgets($fread); $splt = explode("\t", trim($line)); if(count($splt)<4) { continue; } $this->finfo[$splt[0]] = array($splt[1], $splt[2],$splt[3]); } fclose($fread); return count($this->finfo); } private function JobReport($flist) { global $as_iface; $msg = $reason = ''; if(is_array($flist) && count($flist)>0) { foreach($flist as $fname=>$code) { $locode = $code & 0xF; $hicode = ($code & 0xFF0); switch($locode) { case self::NEW_FILE: $reason = $this->_titles['file_is_new']; break; case self::CHANGED_FILE: $reason = $this->_titles['file_changed']; break; case self::SUSPICIOUS_FILE: $reason = $this->_titles['file_suspicious'] . ' : ' . $this->_found_signature; break; } if($hicode == self::FILE_WAS_RESTORED) $reason .= ', ' . $this->_titles['file_restored']; elseif($hicode == self::FILE_RESTORE_ERROR) $reason .= ', ' . $this->_titles['file_restore_err']; $resoredcd = self::FILE_WAS_RESTORED; $msg .= "$fname - $reason<br />\n"; # $code = $hicode=$resoredcd | $locode, } } else $msg = $this->_titles['file_nochanges']; if($this->_email) { @mail($this->_email,$this->_titles['message_subj'], strip_tags($msg)); } return $msg; } /** * Checks existing files : compares their time/size (and hash-summ) with saved info. * @param integer $auto_restore sets level of auto-restoring : 0(false) - none, * CSitePagesGuard::RESTORE_ONLY_SUSPICIOUS - restore only "suspicious" changed files, * CSitePagesGuard::RESTORE_ALL_CHANGED - restore all files that were unexpectedly changed * @param integer $restore_deleted to auto-restore deleted files * @param mixed $report 1 to return text report about performed job * Returns associative array['filename'=>update_type,...) or false if no data about files gathered yet */ public function CheckFiles($auto_restore = 0, $restore_deleted=false, $report=true) { $retarray = array(); if(count($this->finfo)<1) $this->__LoadFilesInfo(); if(count($this->finfo)<1) { $this->_errormessage = 'No registered files info'; return false; } $actualfiles = array(); foreach($this->_folders as $onefolder) { $actualfiles = array_merge($actualfiles,$this->GetFilesInFolder($onefolder)); } foreach($actualfiles as $fname) { $b_changed = $b_susp = $n_new = 0; $b_restore = false; if(!isset($this->finfo[$fname])) { $retarray[$fname] = self::NEW_FILE; $b_new = true; } else { if( filesize($fname)!=$this->finfo[$fname][0] || filemtime($fname)!=$this->finfo[$fname][1] ) { $b_changed = $retarray[$fname] = self::CHANGED_FILE; } if(!$b_changed && ($this->_fullcheckmode)) { # full check - compare current file md5 summ with saved one $md5sum = md5_file($fname); if($md5sum != $this->finfo[$fname][2]) { $b_changed = $retarray[$fname] = self::CHANGED_FILE; } } if($b_changed) $b_restore = ($auto_restore>= self::RESTORE_ALL_CHANGED); } if(!$b_restore && ($b_changed) && count($this->_mwsignatures)>0 && filesize($fname)>5) { # If "virus-signature" words found in changed file, it will be marked as suspicious $b_susp = $this->IsFileSuspicious($fname); if($b_susp) { $retarray[$fname] = self::SUSPICIOUS_FILE; $b_restore = ($auto_restore> self::RESTORE_NONE); } } if($b_restore) { #<4> if(!empty($this->_backupfolder)) { #<5> $result = $this->__UnpackFile($this->finfo[$fname][2],$fname); $retarray[$fname] |= ($result)? self::FILE_WAS_RESTORED : self::FILE_RESTORE_ERROR; } #<5> elseif(filesize($fname)>$this->finfo[$fname][0]) { #<5A> There's no backup, so try to restore by cutting out added bytes $cleanbody = @file_get_contents($fname); $cleanbody = substr($cleanbody,0,$this->finfo[$fname][0]); if(md5($cleanbody)===$this->finfo[$fname][2]) { # md5 is OK, so cutting last bytes should restore original file $restored = @file_put_contents($fname, $cleanbody); if($restored) @touch($fname,$this->finfo[$fname][1]); $retarray[$fname] |= ($restored==$this->finfo[$fname][0])? self::FILE_WAS_RESTORED : self::FILE_RESTORE_ERROR; } else $retarray[$fname] |= self::FILE_RESTORE_ERROR; } #<5A> } } $delrestored = ''; if($restore_deleted) { $delrestored = $this->RestoreDeletedFiles($report); } if($report) { $ret = ($this->JobReport($retarray) . $delrestored); if($ret) $ret .="\n<br />"; return $ret; } return $retarray; } private function __SaveFilesInfo() { $fout = @fopen($this->_datafile,'w'); $written = false; if($fout) { foreach($this->finfo as $fname => $data) { $written = @fwrite($fout, ($fname . "\t" . $data[0] . "\t" . $data[1]. "\t" . $data[2] . "\n")); if($written===false) break; } @fclose($fout); } return ($written!=false); } private function __CleanBackupfolder() { if(empty($this->_backupfolder)) return false; foreach(glob($this->_backupfolder.'/*.gz') as $fname) { unlink($fname); } } public function IsFileSuspicious($fname) { global $as_iface; if(count($this->_mwsignatures)<1 || filesize($fname)<4 ) return false; $body = @file_get_contents($fname); if($body===false) { echo ($this->_errormessage = $fname . ' - '. (isset($as_iface['err_read_file'])? $as_iface['err_read_file']:'Reading file error')); return false; } $b_susp = false; foreach($this->_mwsignatures as $vname=>$vsignature) { if(stripos($body,$vsignature)!==false) { $this->_found_signature = "($vname)"; return (is_string($vname)? $vname: true); } } return false; } public function GetErrorMessage() { return $this->_errormessage; } public function GetStatistics() { return $this->_stats; } } # CSitePagesGuard() definition end