<?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
|