<?php 
/**
 *       @file  qtrReport.php
 *      @brief  This module contains investigation report
 *
 *     @author  Quttera (qtr), contactus@quttera.com
 *
 *   @internal
 *     Created  01/14/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( ABSPATH . WPINC . '/ms-functions.php' );

require_once('qtrConfig.php');
require_once('qtrOptions.php');
require_once('qtrLogger.php');
require_once('qtrStats.php');
require_once('qtrIgnoreList.php');
require_once('qtrThreatsWhiteList.php');
require_once('qtrFilesWhiteList.php');

define( 'QTR_REPORT','quttera_wp_report');

class CQtrReport
{
    protected $_report;
    protected $_config;

    public function __construct( $type = NULL )
    {
        $this->_config      = new CQtrConfig();
        $this->_logger      = new CQtrLogger();
        $this->_report      = array();
        $this->_report_file = dirname(__FILE__) . DIRECTORY_SEPARATOR . QTR_REPORT . ".txt";
        $this->_ignore_list          = new CQtrIgnoreList();
        $this->_files_white_list     = new CQtrFilesWhiteList();
        $this->_threats_white_list   = new CQtrThreatsWhiteList();
        $this->_LoadReport();
 
    }

    public function AddFileReport(  $engine,
                                    $severity,
                                    $file_path,
                                    $file_sig,
                                    $threat_sig, 
                                    $threat, 
                                    $details, 
                                    $name, 
                                    $autofix_enabled=false, 
                                    $autofix_succeeded=false)
    {
        $entry = array();
        $entry["ENGINE"]    = $engine;
        $entry["SEVERITY"]  = $severity;

        if( !$file_sig )
        {
            $entry["FILE_MD5"] = md5_file( $file_path );
        }
        else
        {
            $entry["FILE_MD5"] = $file_sig;
        }

        $file = substr($file_path ,strlen(ABSPATH));
        $entry["FILE"] = $file;

        $threat = preg_replace("/\s\s*/"," ", $threat);
        $threat = preg_replace("/\r\n/","", $threat);
        $threat = preg_replace("/<\?/","&#60;?", $threat);
        $threat = preg_replace("/\?>/","?&#62;", $threat);
        $threat = substr($threat,0,60) . "...";

        $entry["THREAT_NAME"]   = $name;
        $entry["THREAT_SIG"]    = $threat_sig;
        $entry["THREAT"]        = $threat;
        $entry["DETAILS"]       = $details;

        if( $autofix_enabled ){
            $entry["FIX_ENABLED"] = "Yes";
        }else{
            $entry["FIX_ENABLED"] = "No";
        }

        if( $autofix_succeeded ){
            $entry["FIX_SUCCEEDED"] = "Yes";
        }else{
            $entry["FIX_SUCCEEDED"] = "No";
        }

        if( $this->_IsWhiteListed($entry) == FALSE ){
            array_push( $this->_report,$entry );
            $this->_StoreReport();
        }
        return TRUE; 
    }


    public function AddDbReport( $engine,$severity,$database,$table,$row,$threat_name,$threat_sig,$threat )
    {
        $entry = array();
        $entry["ENGINE"]    = $engine;
        $entry["SEVERITY"]  = $severity;
        $entry["DATABASE"]  = $database;
        $entry["TABLE"]     = $table;
        $entry["ROW"]       = $row;
        $entry["THREAT_NAME"] = $threat_name;
        $entry["THREAT_SIG"] = $threat_sig;
        $threat = str_replace("\n","",$threat);
        $threat = preg_replace("/\s\s*/"," ", $threat);
        $threat = preg_replace("/\r\n/","", $threat);
        $threat = substr($threat,0,60) . "...";

        // Sanitize to valid UTF-8 — raw DB values can contain arbitrary encodings
        // that would cause json_encode() to silently return false
        if ( function_exists('mb_convert_encoding') ) {
            $threat = mb_convert_encoding($threat, 'UTF-8', 'UTF-8');
        } else {
            $threat = htmlspecialchars_decode(
                htmlspecialchars($threat, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'),
                ENT_QUOTES
            );
        }

        $entry["THREAT"]     = trim($threat);

        $this->_logger->Info(sprintf(
            'Adding DB entry: table=%s, row=%s, threat_name=%s, severity=%s',
            $table, $row, $threat_name, $severity
        ));

        if($this->_IsWhiteListed($entry) == FALSE ){
            array_push( $this->_report, $entry );
            $this->_logger->Info(sprintf('DB entry stored to report (total entries: %d)', count($this->_report)));
            $this->_StoreReport();
        }
        return TRUE; 
    }

    public function GetThreat( $file_sig,$threat_sig )
    {
        $this->_LoadReport();
        $index = 0;
        foreach( $this->_report as $entry )
        {
            if( $entry["FILE_MD5"] == $file_sig && $entry["THREAT_SIG"] == $threat_sig )
            {
                return $entry;
            }
        }
        /*
         * Threat not found
         */
        return NULL;
    }

    public function RemoveThreat( $file_sig,$threat_sig )
    {
        $this->_LoadReport();
        $index = 0;
        foreach( $this->_report as $entry )
        {
            if( $entry["FILE_MD5"] == $file_sig && $entry["THREAT_SIG"] == $threat_sig )
            {
                unset($this->_report[$index]);
                $this->_StoreReport();
                return TRUE;
            }
            else
            {
                $index++;
            }
        }
        /*
         * Threat not found
         */
        return FALSE;
    }

    public function Reset(){    
        $this->_report = array();
        $this->_StoreReport();
        return TRUE;
    }


    public function GenerateMeta(){
        return $this->_GenerateHeader();
    }


    public function GetStats(){

        $stats              = new CQtrStats();
        $clean              = $stats->Total();
        $suspicious         = 0;
        $psuspicious        = 0;
        $malicious          = 0;
        $dump               = $this->GetDetectedThreats();
        $detected_files     = array();

        // DB detected severity counts (post-whitelist)
        $db_suspicious      = 0;
        $db_psuspicious     = 0;
        $db_malicious       = 0;

        foreach ($dump  as $entry){

            /*
             * Database detections are tracked in separate ROWS_* counters.
             * Skip them here to avoid corrupting file scan statistics.
             */
            if( isset($entry["DATABASE"]) ){
                $sev = strtolower($entry["SEVERITY"]);
                if( strpos($sev,"mal") !== FALSE ){
                    $db_malicious += 1;
                }else if( strpos($sev,"pot") !== FALSE ){
                    $db_psuspicious += 1;
                }else if( strpos($sev,"susp") !== FALSE ){
                    $db_suspicious += 1;
                }
                continue;
            }

            if( array_key_exists("FILE_MD5", $entry) and 
                array_key_exists($entry["FILE_MD5"], $detected_files) ){
                /*
                 * This file already handled
                 */
                continue;
            }

            $severity = $entry["SEVERITY"];
            $sev = strtolower($severity);
            if(strpos($sev,"mal") !== FALSE ){
                $malicious += 1;
                $clean -= 1;
                if(array_key_exists("FILE_MD5", $entry)){
                    $detected_files[$entry["FILE_MD5"]] = "malicious";
                }
            }
            else if(strpos($sev,"pot") !== FALSE ){
                $psuspicious += 1;
                $clean -= 1;
                if(array_key_exists("FILE_MD5", $entry)){
                    $detected_files[$entry["FILE_MD5"]] = "psuspicious";
                }
            }
            else if(strpos($sev,"susp") !== FALSE ){
                $suspicious += 1;
                $clean -= 1;
                if(array_key_exists("FILE_MD5", $entry)){
                    $detected_files[$entry["FILE_MD5"]] = "suspicious";
                }
            }
        }

        // Update file scan counters
        $stats->Clean($clean);
        $stats->Suspicious($suspicious);
        $stats->PotSuspicious($psuspicious);
        $stats->Malicious($malicious);

        // Update DB detected severity counts from the post-whitelist report.
        // ROWS_TOTAL and ROWS_CLEAN are set by the scanner during the scan;
        // recompute ROWS_CLEAN here to reflect whitelisted-out entries.
        $db_detected        = $db_malicious + $db_psuspicious + $db_suspicious;
        $rows_clean         = max(0, $stats->RowsTotal() - $db_detected);
        $stats->RowsClean($rows_clean);
        $stats->RowsSuspicious($db_suspicious);
        $stats->RowsPotSuspicious($db_psuspicious);
        $stats->RowsMalicious($db_malicious);

        return $stats;
    }


    public function GetDetectedThreats(){
        $output             = array();
        /* 
         * remove all ignored threats 
         */
        foreach($this->_report as $threat ){

            if($this->_IsWhiteListed( $threat ) == FALSE ){
                /*
                * This is not ignored threat 
                */
                array_push($output,$threat);
            }
        }

        $this->_logger->Info(sprintf(
            'GetDetectedThreats: Returning %d threats from report (total in report: %d)',
            count($output), count($this->_report)
        ));
        return $output;
    }


    protected function DumpToString(){
        $header = $this->_GenerateHeader();
        $body   = $this->_GenerateBody();
        return $header . "\r\n" . $body;
    }

    public function Get(){
        return $this->_report;
    }


    public function StoreFileReport(){
        return $this->_StoreToFile();
    }

    public function Finalize(){
        $this->_StoreToFile();
        return TRUE;
    }


    /***************************************************************************
     *
     *      PROTECTED METHODS
     *
     **************************************************************************/
    protected function _LoadReport()
    {
        $body   = CQtrOptions::GetOption( QTR_REPORT );

        if( $body )
        {
            $this->_report = CQtrOptions::Unserialize( $body );

            if( !is_array( $this->_report ) ){
                /* 
                 * something gone wrong, reset report
                 */
                $this->_report = array();
            }
        }else{
            /*
             * nothing found
             */
            $this->_report = array();
        }

        return TRUE;
    }

    protected function _StoreReport(){
        /*
         * Overwrite report in file
         */
        $this->_StoreToFile();

        $body = CQtrOptions::Serialize( $this->_report );

        if ( CQtrOptions::GetOption( QTR_REPORT ) !== false ) 
        {
            $rc = CQtrOptions::UpdateOption( QTR_REPORT , $body );
        }
        else 
        {
            $deprecated = null;
            $autoload   = 'no';
            return CQtrOptions::AddOption( QTR_REPORT , $body ,$deprecated, $autoload );
        }
    }   

    protected function _StoreToFile(){

        if(!defined("QTR_DEBUG")){
            return FALSE;
        }

        $header = $this->_GenerateHeader();
        $body   = $this->_GenerateBody();
        $file   = @fopen( $this->_report_file,"w");
        
        if($file === FALSE){
            error_log("Failed to open report file for writing: " . $this->_report_file . ". Check file permissions.");
            return FALSE;
        }
        
        fwrite($file,$header);
        fwrite($file,$body);
        fflush($file);
        fclose($file);
        return TRUE;
    }

    protected function _GenerateHeader(){
        /*
         * Loads statistics
         */
        $stats      = $this->GetStats(); 
        $site       = $this->_GetCurrentSite();
        $timestr    = date('Y-m-d H:i'); 
        $itimestr   = date('Y-m-d H:i', $stats->StartTime());
        $total      = $stats->Total();
        $clean      = $stats->Clean();
        $susp       = $stats->Suspicious();
        $psusp      = $stats->PotSuspicious();
        $malicious  = $stats->Malicious();
        $header     = <<<HEADER

=======================================================================
    Quttera Web Malware Scanner plugin for WordPress
    Website Malware Scan Report 
    
    Scanned Website:        $site    
    Scan type:              Internal
    Report generation time: $timestr

    Scan launch time:       $itimestr    
    Scanned files:          $total
    Clean:                  $clean
    Potentially Suspicious: $psusp
    Suspicious:             $susp
    Malicious:              $malicious
    
    © 2024 Quttera Ltd. All rights reserved.
    For any questions about this report: support@quttera.com
=======================================================================
\r\n
HEADER;
        return $header;
    }

    protected function _GenerateBody()
    {
        $body = "";
        foreach ( $this->_report  as $entry){
            $threat = $entry["THREAT"];

            $threat = preg_replace("/\s\s*/"," ", $threat);
            $threat = preg_replace("/\r\n/","", $threat);
            $threat = substr($threat,0,60) . "...";
            $body .= "\r\n\r\n";

            // Check if this is a file entry or database entry
            if (isset($entry["FILE"])) {
                // File entry
                $body .= "FILE:         " . $entry["FILE"] . "\r\n";
                $body .= "FILE_MD5:     " . $entry["FILE_MD5"] . "\r\n";
                $body .= "DETAILS:      " . $entry["DETAILS"] . "\r\n";
            } elseif (isset($entry["DATABASE"])) {
                // Database entry
                $body .= "DATABASE:     " . $entry["DATABASE"] . "\r\n";
                $body .= "TABLE:        " . $entry["TABLE"] . "\r\n";
                $body .= "ROW:          " . $entry["ROW"] . "\r\n";
            }

            $body .= "SEVERITY:     " . $entry["SEVERITY"] . "\r\n";
            $body .= "ENGINE:       " . $entry["ENGINE"] . "\r\n";
            $body .= "THREAT_SIG:   " . $entry["THREAT_SIG"] . "\r\n";
            $body .= "THREAT_NAME:  " . $entry["THREAT_NAME"] . "\r\n";
            $body .= "THREAT:       " . $threat . "\r\n";
        }

        return $body;
    }

    protected function _GetCurrentSite(){

        if(!function_exists('get_site_url') ){
            /*
             * running outside of WP
             */
            return "example.com";
        }else{
            //return get_current_site()->site_name;
            return get_site_url();
        }
    }


    public function _IsWhiteListed( $threat ){
        if($this->_files_white_list->IsLoaded() == FALSE ){
            $this->_files_white_list->Load();
        }

        // DATABASE entries have no FILE_MD5 — check by THREAT_SIG only, skip file whitelist
        if( isset($threat["DATABASE"]) ){
            if( $this->_ignore_list->Get( "", $threat["THREAT_SIG"] ) != NULL ){
                return TRUE;
            }
            if( $this->_threats_white_list->Get( "", $threat["THREAT_SIG"] ) != NULL ){
                return TRUE;
            }
            return FALSE;
        }

        // FILE entries — original behavior
        $file_md5 = isset($threat["FILE_MD5"]) ? $threat["FILE_MD5"] : "";

        if( $this->_ignore_list->Get( $file_md5, $threat["THREAT_SIG"] ) != NULL ){
            return TRUE;
        }

        if( $this->_threats_white_list->Get( $file_md5, $threat["THREAT_SIG"] ) != NULL ){
            return TRUE;
        }

        if( $this->_files_white_list->IsWhiteListed( $file_md5 ) ){
            return TRUE;
        }
        
        return FALSE;
    }

}


?>
