Location: PHPKode > projects > PHP Security Scanner > securityscanner-1.0.2/bin/Security_Scanner.php
<?php
error_reporting(E_ALL);

    # PHP Security Scanner
    # This program is distributed under the terms and conditions of the GPL
    # See the README and LICENSE files for details

    # This file contains the main class of the tool.

/**
 * Description: Security_Scanner class is used in PHP Security Scan project
 *
 * PHP version 4 - should work with php 5 too
 *
 * LICENSE: This source file is subject to the GPL license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to hide@address.com so we can mail you a copy immediately.
 *
 * @category    Security
 * @package    PHP Security Scanner
 * @author     Julian Davtchev <hide@address.com>
 * @license     http://opensource.org/licenses/gpl-license.php  GPL license
 * @since      Class available since Release 1.0
 */

class Security_Scanner
{
    // {{{ properties

    /**
     * The searched path
     *
     *  @var string
     *  @access private
     *  @see Security_Scanner::Security_Scanner()
     */
    var $_path = '';

    /**
     * Array with problem and the descriptions. Used later in the interface
     * @see Security_Scanner::preparePatterns()
     * @access private
     * @var array
     */
    var $problem_description = array();

    /**
     *  Optimized patterns, ready to be searched for.
     *  @see Security_Scanner::preparePatterns()
     *  @see Security_Scanner::searchFileForPattern()
     *  @var array
     */
    var $prapared_patterns = array();

     /**
     *  @see Security_Scanner::addResult()
     *  @var array $_search_results holds search result information
     */
    var $_search_results = array();

     /**
     * @var array $inaccessible Contains not accessible files or empty files and not accessible directories.
     */
    var $inaccessible = array();

    // }}}
    // {{{ Security_Scanner

    /**
    *   Constructor
    *
    *   Initiates path and label.
    *
    *   @param string $path Directory/File to search for.
    *   @param string $label Label of the search...used later for interface.
    *   @uses Security_Scanner::$_path   Sets initial values
    *   @uses Security_Scanner::$_label    Sets initial values
    *   @uses Security_Scanner::formatDirectoryName() Formats directory name without trailing slashes
    *   @access public
    *
    */
    function Security_Scanner($path,$label)
    {
        $this->_path = $this->formatDirectoryName($path);
        $this->_label = (string) $label;
    }

    // }}}
    // {{{ formatDirectoryName

    /**
    *   Removes trailing slashes from directory names. This method is called once.
    *
    *   @param string $path Directory/File to search for.
    *   @see Security_Scanner::Security_Scanner()
    *   @return string Directory without trailing slash
    *   @access private
    *
    */
    function formatDirectoryName ($dir)
    {
        $dir = preg_split('/\/+$/',$dir,-1,PREG_SPLIT_NO_EMPTY);
        return $dir[0];
    }

    //}}}
    // {{{ preparePatterns()

    /**
    *
    *   Patterns are prepared so that only one search is executed for close patterns...
    *   e.g.
    *   problem -  pattern_expression
    *   text       -  %(text)reg_exp_chars%
    *   text2     -  %(text2)reg_exp_chars%
    *   This will result in the following pattern
    *   %(text|text2)reg_exp_chars%   -> since the only thing they differ in is the problem string.
    *
    *   Array with problem and problem description for interface usage. This is necessery since
    *   prepared_patterns gather every problem in one and thus, have to decide which pattern from the result
    *   belongs to which description.
    *   @param array $db_pattern_data  The problem, problem description and pattern_expression taken from DB.
    *   DB data. Have in mind that it is not empty. So it is checked before used in the class.
    *   @todo  don't have to be so complicated function
    *
    *   @uses Security_Scanner::$problem_description
    *   @uses Security_Scanner::$prapared_patterns
    *   @access public
    */
    function preparePatterns($db_pattern_data)
    {
        for ($i = 0, $cnt = count($db_pattern_data); $i < $cnt; $i++) {
            if (!isset($empty_pattern)) {
                $empty_pattern = array();
            }
            $empty_pattern[$db_pattern_data[$i]['problem']] = preg_replace("/".$db_pattern_data[$i]['problem']."/","____",$db_pattern_data[$i]['pattern_expression']);            $this->problem_description[$i] = array ('problem' => $db_pattern_data[$i]['problem'], 'problem_description' => $db_pattern_data[$i]['problem_description']);
        }
        $pattern_functions = array();
        foreach ($empty_pattern as $k=>$v) {
            if (!isset ($pattern_functions[$v])) {
                $pattern_functions[$v] = '';
            }
                $pattern_functions[$v] .= $k.'|';
        }
        //var_dump($pattern_functions);
        //exit();
        foreach ($pattern_functions as $k => $v) {
            $pattern_functions[$k] = substr($v, 0, -1);
        }

        foreach ($pattern_functions as $k=>$v) {
            $this->prapared_patterns[] = preg_replace("/____/",$v,$k);
        }
//         var_dump($this->prapared_patterns);
//         exit();
    }

    //}}}
    // {{{ searchFileForPattern()

    /**
    *   @param string $file File's path name.
    *   @see Security_Scanner::run()
    *   @uses Security_Scanner::$inaccessible
    *   @uses Security_Scanner::$prapared_patterns
    *   @uses Security_Scanner::getProblemDescription()
    *   @uses Security_Scanner::addResult()
    */
    function searchFileForPattern($file)
    {

        if (!is_readable($file)) {
            if (!isset($this->inaccessible['files']['inaccessible']) || !in_array($file,$this->inaccessible['files']['inaccessible'])) {
                $this->inaccessible['files']['inaccessible'][] = $file;
            }
            return;
        }

        if (!file($file)) {
            if (!isset($this->inaccessible['files']['empty']) || !in_array($file,$this->inaccessible['files']['empty'])) {
                $this->inaccessible['files']['empty'][] = $file;
            }
            return;
        }

        $fileLines = file($file);
            foreach ($this->prapared_patterns as $pattern) {
                foreach($fileLines as $lineNumber=>$lineContent) {
                    $lineContent = trim($lineContent);
                    if(preg_match($pattern,$lineContent)) {
                        $problemDescription = $this->getProblemDescription($lineContent);
                        // log result
                        $this->addResult($file, $lineContent, $lineNumber+1, $problemDescription);
                    }
                }
            }
    }

    //}}}
    // {{{ scanDirectory()

    /**
    *   Scan the directories for files and directories and makes sure they obey matching rules from the config file
    *   @param string Directory name
    *   @see Security_Scanner::run()
    *   @uses Security_Scanner::isMatchingList()
    *   @uses Security_Scanner::searchFileForPattern()
    *   @uses Security_Scanner::scanDirectory()
    *   @uses Security_Scanner::isExtension()
    *   @uses Security_Scanner::$inaccessible
    */
    function scanDirectory($dir)
    {
        $subDirs = array();
        $dirFiles = array();
        if ($handle = @opendir($dir)) {
            while (false !== ($file = readdir($handle))) {
                if ($file != "." && $file != ".." ) {
                    if (is_dir($dir."/".$file) && $this->isMatchingList($file,'dir')) {
                            $subDirs[] = $dir."/".$file;
                    } elseif (is_file($dir."/".$file) && $this->isExtension($file)) {
                        if ($this->isMatchingList($file,'file')) {
                            $dirFiles[] = $dir."/".$file;
                        }
                    } else {
                            ///neither file, nor directory >>> not interested.
                    }
                }
            }
            closedir($handle);
            foreach($dirFiles as $file) {
                    $this->searchFileForPattern($file);
            }
            if (count($subDirs) > 0) {
                foreach ($subDirs as $subDir) {
                    $this->scanDirectory($subDir);
                }
            }
        } else {
            if (!isset($this->inaccessible['dirs']['inaccessible']) || !in_array($dir,$this->inaccessible['dirs']['inaccessible'])) {
                $this->inaccessible['dirs']['inaccessible'][] = $dir;
            }
        }

    }

    //}}}
    // {{{ isExtension()

    /**
    *   Check if file name has extension defined in the configuration file. Usually this would be .php
    *
    *   @param string $file File name to check
    *   @return bool Whether or not file has PHP_EXTENSION extension
    *   @access private
    *   @see Security_Scanner::scanDirectory()
    *
    */
    function isExtension($file)
    {
        $extension = $GLOBALS['php_extensions'];
        $file_ext = array_pop(explode(".",$file));
        if (in_array($file_ext,$extension)) {
            return true;
        }
        return false;
    }

    //}}}
    // {{{ isMatchingList()

    /**
    *   Finds if file/directory(depending on $type) is ok to be scanned. This method takes data from
    *   configuration method. Data is taken from configuation file.
    *
    *   @param string $file This is returned from the dir handle. Could be filename or dirname
    *   @param string $type What type are we scanning: file or directory.
    *   @return bool If file/dir maches the white/black list defined in the configuration file
    *   @see Security_Scanner::scanDirectory
    *   @uses Security_Scanner::isMatch
    *
    *   -------------------------------------------------
    *   |Matches                |White list | Black list|
    *   -------------------------------------------------
    *   | Any white/but black   |   set     |   set     |
    *   -------------------------------------------------
    *   | All/ but black        |   not set |   set     |
    *   -------------------------------------------------
    *   | Only white (any of it)|   set     |  not set  |
    *   -------------------------------------------------
    *   | All                   |   not set |  not set  |
    *   -------------------------------------------------
    *
    *    set       >>> array is not empty and scip_list == false
    *    not set   >>> array is empty or scip_list == true
    *
    */
    function isMatchingList($file, $type)
    {
        /*
        * Will search if file
        */
        if ($type === 'file') {
            $white_list = $GLOBALS['match_list']['file']['white_list'];
            $black_list = $GLOBALS['match_list']['file']['black_list'];
            //striping the extension from file's name (it is sure to be one of PHP_EXTENSION)
            //+1 is for the "." dot.
            $ext_lenght = strlen(array_pop(explode(".",$file))) + 1;
            $file = substr($file,0, -$ext_lenght);
        }
        /*
        * Will search if directory
        */
        if ($type === 'dir') {
            $white_list = $GLOBALS['match_list']['dir']['white_list'];
            $black_list = $GLOBALS['match_list']['dir']['black_list'];
        }

        //we have the white and black lists. Algorithm for finding directoriy and file match is the same.
        /*
        *   |Matches                |White list | Black list|
        *   | All                   |   not set |  not set  |
        */
        if ($white_list['skip_list'] !== false && $black_list['skip_list'] !== false) {
            return true;
        }

        /*
        *   |Matches                |White list | Black list|
        *   | Any white/but black   |   set     |   set     |
        */
        if ($white_list['skip_list'] === false && $black_list['skip_list'] === false) {
            return ($this->isMatch($file,$white_list) && !$this->isMatch($file,$black_list));
        }

        /*
        *   |Matches                |White list | Black list|
        *   | All/ but black        |   not set |   set     |
        */
        if ($white_list['skip_list'] !== false && $black_list['skip_list'] === false) {
            return !$this->isMatch($file,$black_list);
        }

        /*
        *   |Matches                |White list | Black list|
        *   | Only white (any of it)|   set     |  not set  |
        */
        if ($white_list['skip_list'] === false && $black_list['skip_list'] !== false) {
            return $this->isMatch($file,$white_list);
        }
    }

    //}}}
    // {{{ isMatch()

    /**
    *   @todo Think of some way not to iterate twice the same array and keep the logic.
    *   @param string $file This is the name of the file/directory to test against white/black lists.
    *   @param array $list Black or white list...the algorithm is the same for both.
    *   @return bool If the name match black/white list. Depends which on the $list.
    *   @see Security_Scanner::isMatchingList()
    *   @uses Security_Scanner::escapePregString()
    *   @access private
    */
    function isMatch ($file, $list) {

        //checking "exact" array.
        if (!empty($list['exact'])) {
            if (in_array($file,$list['exact'])) {
                return true;

            }
        }
        //checking "start_with" array
        if (!empty($list['start_with'])) {
            foreach ($list['start_with'] as $pattern) {
                if (preg_match('%^'.$this->escapePregString($pattern).'%',$file)) {
                    return true;
                }
            }
        }
        //checking "end_on" array....itterating twice so first start_with is checked and then end_on
        if (!empty($list['end_on'])) {
            foreach ($list['end_on'] as $pattern) {
            echo $file."\n";
                if (preg_match('%'.$this->escapePregString($pattern).'$%',$file)) {
                    return true;
                }
            }
        }
        return false;

    }

    //}}}
    // {{{ escapePregString()

    /**
    *   Used in isMatch to escape characters that are not escaped by PHP's preg_quote() function
    *
    *   @param string $file This is the name of the file/directory to test against white/black lists.
    *   @param array $list Black or white list...the algorithm is the same for both.
    *   @return string The string to use in the regular expression.
    *   @see Security_Scanner::isMatch()
    *   @access private
    */
    function escapePregString($string) {
        $string = preg_quote($string);

        //these characters are not escaped by preg_quote(). Just to be on the save side I escape them myself.
        $chars_to_escape = array ('%','@','#','&','/','_');
        $escape_chars_with = array ('\%','\@','\#','\&','\/','\_');
        return str_replace($chars_to_escape,$escape_chars_with,$string);
    }

    //}}}
    // {{{ getProblemDescription()

    /**
    *   Gets the problem description of the problem found in $line
    *
    *   @todo:This is not very correct since will get only one problem description. If
    *   exec ($...system($...)) etc. ->  we actually have more problems on the same line and will return description of the last in the line.
    *   @param string $line Line content
    *   @return string the description of the problem found on particular $line
    *   @see Security_Scanner::searchFileForPattern
    *   @uses Security_Scanner::$problem_description
    *   @access private
    */
    function getProblemDescription ($line)
    {
        for ($i = 0, $cnt = count($this->problem_description); $i < $cnt; $i++) {
            if (strstr($line,$this->problem_description[$i]['problem'])) {
                return $this->problem_description[$i]['problem_description'];
            }
        }
    }

    //}}}
    // {{{ run()

    /**
    *   This is the main method used. This actually initiates the search
    *   @uses Security_Scanner::searchFileForPattern()
    *   @uses Security_Scanner::scanDirectory()
    *   @access public
    */
    function run()
    {
            if(is_file($this->_path)) {
                // run search on the file
                $this->searchFileForPattern($this->_path);
            } elseif (is_dir($this->_path)) {
                // scan directory contents for string
                $this->scanDirectory($this->_path);
            }
    }

    //}}}
    // {{{ addResult()

    /**
    *   Adds results in an array
    *   @param string $filePath Full path of the file
    *   @param string $lineContents Contents of the line...where match if found.
    *   @param int $lineNumber  Line number of the match
    *   @param string $problemDescription   Description of the problem.
    *   @see Security_Scanner::searchFileForPattern()
    *   @uses Security_Scanner::$_search_results
    *   @access private
    */
    function addResult($filePath, $lineContents, $lineNumber,$problemDescription)
    {
        $this->_search_results[] = array('filePath' => $filePath,
                                        'lineContents' => $lineContents,
                                        'lineNumber' => $lineNumber,
                                        'problemDescription' => $problemDescription
                                        );
    }

    //}}}
    //{{{ Getters

    function getSearchResults()
    {
        return $this->_search_results;
    }

    function getInaccessible()
    {
        return $this->inaccessible;
    }

    function getResults()
    {
        return $this->_search_results;
    }

    function getPath()
    {
        return $this->_path;
    }

    //}}}
}


?>
Return current item: PHP Security Scanner