<?php

/**
 *       @file  qtrFilesScanner.php
 *      @brief  This module contains implementation of file scanner
 *
 *     @author  Quttera (qtr), contactus@quttera.com
 *
 *   @internal
 *     Created  01/16/2016
 *    Compiler  gcc/g++
 *     Company  Quttera
 *   Copyright  Copyright (c) 2016, Quttera
 *
 * This source code is released for free distribution under the terms of the
 * GNU General Public License as published by the Free Software Foundation.
 * =====================================================================================
 */

require_once('qtrOptions.php');
require_once('qtrConfig.php');
require_once('qtrLogger.php');
require_once('qtrPatternsDb.php');
require_once('qtrReport.php');
require_once('qtrIgnoreList.php');
require_once('qtrStats.php');
require_once('qtrExecSemaphore.php');
require_once('qtrScanLock.php');
require_once('qtrFilesWhiteList.php');
require_once('qtrThreatsWhiteList.php');
require_once('qtrMimetype.php');

@ini_set('max_execution_time', 30000 );
@ini_set('max_input_time', 30000 );
@ini_set('memory_limit', '2024M');
@set_time_limit(30000);

class CQtrFilesScanner
{
    protected $_logger;
    protected $_patterns_db;
    protected $_report;
    protected $_config;
    protected $_stats;
    protected $_exec_sem;
    protected $_files_white_list; 

    public function __construct()
    {
        $this->_logger              = new CQtrLogger();
        $this->_patterns_db         = new CQtrPatternsDatabase();
        $this->_report              = new CQtrReport();
        $this->_config              = new CQtrConfig(); 
        $this->_stats               = new CQtrStats();
        $this->_ignore_list         = new CQtrIgnoreList();
        $this->_files_white_list    = new CQtrFilesWhiteList();
        $this->_mime_filer          = new CQtrMimetype(); 
        $this->_last_report_dump    = time();
    }


    public function Initialize( $args = NULL )
    {
        $dbpath = dirname(__FILE__) . DIRECTORY_SEPARATOR . $this->_config->PatternsDbName();
        if( is_file($dbpath))
        {
            $rc = $this->_patterns_db->Load($dbpath);
            if( !$rc ){
                $this->_logger->Error(sprintf("Failed to load pattern database, File %s not found", $dbpath));
            }else{
                $this->_logger->Info(sprintf("Patterns database %s loaded successfully", $dbpath));
            }
        }else{
            $this->_logger->Error( "Failed to locate name of patterns database" );
        }

        $this->_files_white_list->Load();
        return TRUE;
    }

    public function Finalize( $argv = NULL )
    {
        $this->_report->Finalize();
        return TRUE;
    }


    public function Scan( $path )
    {
        @ini_set('max_execution_time', 30000 );
        @ini_set('max_input_time', 30000 );
        @ini_set('memory_limit', '2024M');
        @set_time_limit(30000);

        $this->_report->Reset ( );
        //$this->_ignore_list->Clean( );

        $exec_sem = new CQtrExecSem();
        $exec_sem->ScannerPid( getmypid() );
        $this->_stats->Reset();
        
        $this->_logger->Info(sprintf("Start investigation of %s", $path));

        if( is_dir($path))
        {
            $this->_ScanDirectory($path);
        }
        else if( is_file( $path ))
        {
            $this->_ScanFile($path);
        }
        else
        {
            $this->_logger->Error( "Provided invalid path" ); 
        }
        
        $this->_logger->Info( sprintf("Investigation of %s done",$path));
            
        $exec_sem = new CQtrExecSem();
        $exec_sem->ShouldStop('DONE');
        CQtrScanLock::Release();
        return TRUE;
    }

    public function ScanWordPress( $root_path )
    {
        $this->_report->Reset ( );
        //$this->_ignore_list->Clean( );

        $exec_sem = new CQtrExecSem();
        $exec_sem->ScannerPid( getmypid() );
        $this->_stats->Reset();
        

        /*
         * 1 - Scan wp-includes dir
         * 2 - Scan wp-admin dir
         * 3 - Scan themes dir
         * 3 - Scan root dir not-recursive
         * 4 - Scan root sub-dirs
         */
        $includes_path = $root_path . DIRECTORY_SEPARATOR . "wp-includes";
        $admin_path = $root_path . DIRECTORY_SEPARATOR . "wp-admin";
        $content_path = $root_path . DIRECTORY_SEPARATOR . "wp-content";
        $themes_path = $content_path . DIRECTORY_SEPARATOR . "themes";

        if( is_dir($includes_path)){
            $this->_logger->Info(sprintf("Start investigation of %s directory", $includes_path));
            $this->_ScanDirectory($includes_path);
        }else{
            $this->_logger->Info(sprintf("Failed to locate wp-includes dir at", $root_path));
        }

        if( is_dir($admin_path)){
            $this->_logger->Info(sprintf("Start investigation of %s directory", $admin_path));
            $this->_ScanDirectory($admin_path);
        }else{
            $this->_logger->Info(sprintf("Failed to locate wp-admin dir at", $root_path));
        }

        if( is_dir($themes_path)){
            $this->_logger->Info(sprintf("Start investigation of %s directory", $themes_path));
            $this->_ScanDirectory($themes_path);
        }else{
            $this->_logger->Info(sprintf("Failed to locate themes dir at", $root_path));
        }

        $this->_logger->Info(sprintf("Start investigation of %s directory", $root_path));

        $this->_ScanDirectory($root_path, false /*not recursive*/);

        $this->_ScanDirectory($root_path, true /*recursive*/);
        
        $this->_logger->Info( sprintf("Investigation of %s done",$root_path));
            
        $exec_sem = new CQtrExecSem();
        $exec_sem->ShouldStop('DONE');
        CQtrScanLock::Release();
        return TRUE;
    }


    public function ScanDirectory($path, $recursive=true)
    {
        //@set_time_limit(0);

        $this->_logger->Info(
            sprintf("Start investigation of directory %s", $path));

        $files = scandir($path);

        $this->_logger->Info(
            sprintf("Directory %s contains %d elements", $path,count($files)));

        foreach($files as $file )
        {
            if( $file == "." or $file == ".."){
                continue;
            }

            $curr_path = $path . DIRECTORY_SEPARATOR .$file;

            if( $recursive and is_dir( $curr_path )){

                $this->ScanDirectory( $curr_path );

            } else if( is_file( $curr_path ) ) {

                $this->ScanFile( $curr_path );
            } else {
                $this->_logger->Info(sprintf("Skipping %s", $curr_path ));
            }
        }

        $this->_logger->Info(sprintf("Investigation of %s done", $path));
    }


    public function ScanFile($path)
    {
        if(!is_file($path)){
            return NULL;
        }
        
        $this->_logger->Info(sprintf("Starting scan of %s", $path ));

        if( strpos($path,"quttera_wp_report") ){
            /*
             * skip investigation generated report
             */
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("%s is clean", $path));
            return FALSE;
        }
 
        $fmd5 = md5_file($path);
        if( $this->_files_white_list->IsWhiteListed( $fmd5 ) ){
            /*
             * This is whitelisted file
             */
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("%s is clean", $path));
            return FALSE;
        }

        /*
         * FIXME - test if this is not core file or add all core files to files white list
         * FIX   - All core files added to whitelist
         */

        /*
         * If this is not text file return
         */
        if( strcmp($this->_mime_filer->CheckMimeType($path), "textfile") !== 0 )
        {
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("BIN file [%s] is clean", $path));
            return FALSE;
        }
   
        /*
         * matches is map between pattern (CQtrPattern) and found match
         */
        $matches = $this->_patterns_db->Scan($path);
        /*
         * remove wordpress location part for logging purposes
         */
        if( $matches ) {
            $logged = FALSE;
            foreach($matches as $match ){
                $pattern = $match[0];
                $md5 = md5($match[1]);

                /*
                if( $this->_threats_white_list->Get($fmd5,$md5) != NULL ){
                    // This threat whitelisted
                    $this->_logger->Info("$fmd5:$md5 is whitelisted");
                    continue;
                }*/

                $this->_report->AddFileReport (
                    "fscanner", 
                    $pattern->severity(),
                    $path,
                    $md5, 
                    $match[1], 
                    $pattern->details()
                );

                $this->_report->StoreFileReport();
                $logged = TRUE;
                $this->_logger->Info( sprintf( "TXT %s is %s", $path,$pattern->severity()) );
                $this->_stats->Increment($pattern->severity());
            }

            if( $logged == FALSE ){
                /*
                 * all threats from this file where whitelisted
                 * report this file as clean
                 */
                $this->_stats->IncClean();
                $this->_logger->Info(sprintf("TXT %s is clean", $path));
                return FALSE;
            }
            return TRUE;

        }else{
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("TXT %s is clean", $path));
            return FALSE;
        }
    }

    /*************************************************************************
     *          PROTECTED METHODS
     ************************************************************************/ 
    protected function _ScanDirectory($path, $recursive=true)
    {
        @set_time_limit(0);

        $this->_logger->Info(
            sprintf("Start investigation of directory %s", $path));

        $files = scandir($path);

        $this->_logger->Info(
            sprintf("Directory %s contains %d elements", $path,count($files)));

        foreach($files as $file )
        {
            if( $file == "." or $file == ".."){
                continue;
            }

            if( $this->_ShouldTerminate() )
            {
                $this->_logger->Info(
                    sprintf("%s noticed termination flag. Terminating.",$path));
                return NULL;
            }

            $curr_path = $path . DIRECTORY_SEPARATOR .$file;

            if( $recursive and is_dir( $curr_path ))
            {
                $this->_ScanDirectory( $curr_path );
            }
            else if( is_file( $curr_path ) )
            {
                $this->_ScanFile( $curr_path );
            }
            else
            {
                $this->_logger->Info(sprintf("Skipping %s", $curr_path ));
            }
        }

        $this->_logger->Info(sprintf("Investigation of %s done", $path));
    }


    protected function _ScanFile($path)
    {
        @set_time_limit(0);

        if( $this->_last_report_dump + 30 < time() ){
            /*
             * Regenerate report to take last changes
             */
            $this->_report->StoreFileReport();
            $this->_last_report_dump = time();
        }

        $this->_logger->Info(sprintf("Starting scan of %s", $path ));

        if( $this->_ShouldTerminate() )
        {
            /*
             * someone raised termination flag
             */
            return NULL;
        }

        if( strpos($path,"quttera_wp_report") ){
            /*
             * skip investigation generated report
             */
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("%s is clean", $path));
            return FALSE;
        }

        if( !is_file($path)){
            $this->_logger->Error(sprintf("Path %s is not a file", $path));
            return NULL;
        }

        $fmd5 = md5_file($path);
        if( $this->_files_white_list->IsWhiteListed( $fmd5 ) )
        {
            //$this->_logger->Info("$path is whitelisted");
            /*
             * This is whitelisted file
             */
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("%s is clean", $path));
            return FALSE;
        }

        /*
         * FIXME - test if this is not core file or add all core files to files white list
         * FIX   - All core files added to whitelist
         */

        /*
         * If this is not text file return
         */
        if( strcmp($this->_mime_filer->CheckMimeType($path), "textfile") !== 0 )
        {
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("BIN file [%s] is clean", $path));
            return FALSE;
        }
   
        /*
         * matches is map between pattern (CQtrPattern) and found match
         */
        $matches = $this->_patterns_db->Scan($path);
        /*
         * remove wordpress location part for logging purposes
         */
        if( $matches ) {
            $logged = FALSE;
            foreach($matches as $match ){
                $pattern = $match[0];
                $md5 = md5($match[1]);

                /*
                if( $this->_threats_white_list->Get($fmd5,$md5) != NULL ){
                    // This threat whitelisted
                    $this->_logger->Info("$fmd5:$md5 is whitelisted");
                    continue;
                }*/

                $this->_report->AddFileReport (
                    "fscanner", 
                    $pattern->severity(),
                    $path,
                    $md5, 
                    $match[1], 
                    $pattern->details()
                );

                $logged = TRUE;
                $this->_logger->Info( sprintf( "TXT %s is %s", $path,$pattern->severity()) );
                $this->_stats->Increment($pattern->severity());
            }

            if( $logged == FALSE ){
                /*
                 * all threats from this file where whitelisted
                 * report this file as clean
                 */
                $this->_stats->IncClean();
                $this->_logger->Info(sprintf("TXT %s is clean", $path));
                return FALSE;
            }
            return TRUE;

        }else{
            $this->_stats->IncClean();
            $this->_logger->Info(sprintf("TXT %s is clean", $path));
            return FALSE;
        }
    }


    public function _ShouldTerminate()
    {
        $rc = CQtrScanLock::IsLocked();
        if( $rc ){
            #$this->_logger->Info("ScanLock is set. Continuing.");
            return FALSE;
        }else{
            #$this->_logger->Info("ScanLock is missing. Terminating.");
            return TRUE;
        }
    }
}


?>
