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: 10 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

For more information send a message to info at phpclasses dot org.