Security scanner simpletest integration

  1. <?php
  2. // $Id: security_scanner.module,v 1.16 2008/07/26 09:42:29 ingo86 Exp $
  3.  
  4. // Including DrupalWebTestCase() class dependencies
  5. include_once drupal_get_path('module', 'simpletest') . '/simpletest/web_tester.php';
  6. include_once drupal_get_path('module', 'simpletest') . '/simpletest/unit_tester.php';
  7. include_once drupal_get_path('module', 'simpletest') . '/drupal_test_case.php';
  8. //include_once drupal_get_path('module', 'simpletest') . '/drupal_unit_tests.php';
  9. include_once drupal_get_path('module', 'simpletest') . '/drupal_web_test_case.php';
  10.  
  11.  
  12. // Make my own class that extends DrupalWebTestCase()
  13. // This has to be removed when this variable will switch to public into the main simpletest module.
  14. class DrupalSecurityScannerClass extends DrupalWebTestCase {
  15.   public $curl_options = array();
  16. }
  17.  
  18.   /**
  19.    * Takes a path and returns an absolute path.
  20.    *
  21.    * @param @path
  22.    *   The path, can be a Drupal path or a site-relative path. It might have a
  23.    *   query, too. Can even be an absolute path which is just passed through.
  24.    * @return
  25.    *   An absolute path.
  26.    */
  27.   function getAbsoluteUrl($path) {
  28.     $options = array('absolute' => TRUE);
  29.     $parts = parse_url($path);
  30.     // This is more crude than the menu_is_external but enough here.
  31.     if (empty($parts['host'])) {
  32.       $path = $parts['path'];
  33.       $base_path = base_path();
  34.       $n = strlen($base_path);
  35.       if (substr($path, 0, $n) == $base_path) {
  36.         $path = substr($path, $n);
  37.       }
  38.       if (isset($parts['query'])) {
  39.         $options['query'] = $parts['query'];
  40.       }
  41.       $path = url($path, $options);
  42.     }
  43.     return $path;
  44.   }
  45.  
  46.   /**
  47.    * Implementation of hook_menu().
  48.    */
  49.   function security_scanner_menu() {
  50.     $items['admin/settings/security_scanner'] = array(
  51.       'title' => 'Security Scanner',
  52.       'page callback' => 'page_security_scanner',
  53.       'access arguments' => array('access scanner'),
  54.       'type' => MENU_NORMAL_ITEM,
  55.     );
  56.   return $items;
  57.   }
  58.  
  59.   /**
  60.    *  Settings page for security scanner module
  61.    */
  62.   function page_security_scanner() {
  63.         return drupal_get_form('security_scanner_form');
  64.   }
  65.  
  66.   /**
  67.    *  Form page into security scanner
  68.    */
  69.   function security_scanner_form($form_state) {
  70.     $options = array('0' => t('Crawl'), '1' => t('Seed'), '2' => t('ReCrawl'));
  71.     $form['settings']['mode'] = array(
  72.       '#type' => 'radios',
  73.       '#title' => t('Mode'),
  74.       '#default_value' =>  variable_get('security_scanner_settings', 0),
  75.       '#options' => $options,
  76.       '#description' => t('Set the mode that the security scanner will works.'),
  77.     );
  78.     $form['submit'] = array(
  79.       '#type' => 'submit',
  80.       '#value' => 'Submit',
  81.     );
  82.     return $form;
  83.   }
  84.  
  85.   /**
  86.    * security_scanner_form_submit():
  87.    * Provide submit handler to settings form;
  88.    */
  89.   function security_scanner_form_submit($form, &$form_state) {
  90.     variable_set('security_scanner_settings', $form_state['values']['mode']);
  91.     drupal_set_message(t('Your settings has been saved.'));
  92.   }
  93.  
  94.   /**
  95.    *  Implementation of the crawler function.
  96.    *  Flow:
  97.    *  1- set crawler_id but leave status = 0
  98.    *  2- make status = 1
  99.    *  3- make status = 2            
  100.    */    
  101.   function crawler_framework($selection_query, $initial_status, $function_callback) {
  102.     // Initialize the crawler
  103.     db_query("INSERT INTO {crawler} VALUES (default)");
  104.     $crawler_id = db_last_insert_id('crawler', 'id');
  105.     $selection_query += array(
  106.       'join' => '',
  107.       'where' => '',
  108.       'parameters' => array(),
  109.     );
  110.     $fields = empty($selection_query['fields']) ? '' : ','. implode(', ', $selection_query['fields']);
  111.     $sql = 'SELECT l.path'. $fields .' FROM {crawler_links} AS l '. $selection_query['join'] .' WHERE crawler_id = %d and status = %d' . $selection_query['where'];
  112.     array_unshift($selection_query['parameters'], $crawler_id, $initial_status+1);
  113.     $time = time() + 40;
  114.     while (time() < $time) {
  115.       $status = $initial_status;
  116.       //Mark the extracted page as visited
  117.       $status++;
  118.       db_query("UPDATE {crawler_links} SET crawler_id = %d, status = %d WHERE status = %d LIMIT 1", $crawler_id, $status, $initial_status);
  119.       // Get the link from crawler_links table
  120.       $selected_results = db_fetch_array(db_query_range($sql, $selection_query['parameters'], 0, 1));
  121.       // Update the status field to sign as executed that link
  122.       // (The following two lines could be move to the end of the function i think without problems)
  123.       db_query("UPDATE {crawler_links} SET status = status + 1 WHERE status = %d AND crawler_id = %d", $status, $crawler_id);
  124.       $status++;
  125.       // Create a new object and parse the page
  126.       $obj = new DrupalSecurityScannerClass();
  127.       // Set the cookie
  128.       $session_cookie = variable_get('security_scanner_cookie','');
  129.       $obj->curl_options = array(
  130.         CURLOPT_COOKIE => $session_cookie,
  131.         CURLOPT_USERAGENT => 'security_scanner',
  132.       );
  133.       $obj->drupalGet($selected_results['path']);
  134.       $obj->parse();
  135.       $function_callback($obj, $selected_results);
  136.       $obj->curlClose();
  137.     }
  138.  }
  139.  
  140.   /**
  141.    *  Crawler: page processing function
  142.    */    
  143.   function security_scanner_page_processing($obj, $selected_results) {
  144.     global $base_url;
  145.     $links = $obj->elements->xpath('//a');
  146.     foreach($links as $link) {
  147.       $url_to_save = (string)$link->attributes()->href;
  148.       $absolute = getAbsoluteUrl($url_to_save);
  149.       // Get the page but check:
  150.       // a - if it's logout link, that makes me lose the cookie.
  151.       // b - if it's security scanner, skip
  152.       // c - if it's xss_injector, skip. That will launch the crawler
  153.       // d - if it's cron.php, that will make a loop
  154.       $parsed_url = parse_url($absolute);
  155.       if (($parsed_url['query'] != 'q=logout') && ($parsed_url['query'] != 'q=admin/settings/security_scanner') && ($parsed_url['query'] != 'q=admin/settings/xss_injector') && ($parsed_url['file'] != 'cron.php')) {  
  156.               if (substr($absolute, 0, strlen($base_url)) == $base_url) {
  157.                 // Here we use IGNORE to insert only one time a link into the table. ("path" is a unique index)
  158.           db_query("INSERT IGNORE INTO {crawler_links} (id, path, crawler_id, status) VALUES ('','%s','','')", $absolute);
  159.         }
  160.       }
  161.     }
  162.     // Get the forms inside the page
  163.     $inputs = $obj->elements->xpath("//input[@name='form_id']");
  164.     foreach($inputs as $input) {
  165.       $form_id = (string)$input->attributes()->id;
  166.       // Debug line! HAS TO BE REMOVED
  167.       echo $form_id."Form inserted! <br />";
  168.       // Here we use again IGNORE to insert only one time a form_id into the table. ("form_id" is the primary key)
  169.       db_query("INSERT IGNORE INTO {crawler_forms} VALUES ('%s','%d')", $form_id, $selected_results['id']);
  170.     }
  171.   }
  172.  
  173.   /**
  174.    *  Implementation of the crawler page.
  175.    */    
  176.   function security_scanner_cron() {
  177.     //  Check if this is the mode that we set from settings page.
  178.     if (variable_get('security_scanner_settings','') == '0') {
  179.       //  Check if the auth session cookie value is already into the db, otherwise call
  180.       //  the function that retrieve this (enable multithreading)
  181.       if (variable_get('security_scanner_cookie','') == '') {
  182.         drupal_security_scanner_get_auth_cookie();
  183.       }
  184.       // Preparing the query that has to be passed to the crawler_framework
  185.       $selection_query = array(
  186.         'fields' => array('l.id'),
  187.       );
  188.       echo "I'm crawling...<br />";
  189.       crawler_framework($selection_query, 0, 'security_scanner_page_processing');
  190.     }
  191.   }
  192.  
  193.   /**
  194.    *  Get the cookie of the admin and insert the first link into the table crawler_links.
  195.    *  There is an issue, I have to start the crawler from uid different than 1.    
  196.    */
  197.    function drupal_security_scanner_get_auth_cookie() {
  198.     $initial_path = user_pass_reset_url(user_load(1));
  199.     // Add sleep to go round a bug inside a drupal core function. Remove it when it's changed into core.
  200.     sleep(1);
  201.     //  Create a new object, set cURL options to call the function drupal_security_scanner_curl_headers that
  202.     //  saves into the variable table the admin cookie. Then set the cookie.
  203.     $obj = new DrupalSecurityScannerClass();
  204.     $obj->curl_options = array(
  205.       CURLOPT_HEADERFUNCTION => 'drupal_security_scanner_curl_headers',
  206.       CURLOPT_FOLLOWLOCATION => 0,
  207.     );
  208.     // Get the page with password reset and push submit button
  209.     $obj->drupalGet($initial_path);
  210.     $obj->drupalPost($initial_path,'',TRUE);
  211.     //  Add the first url into the crawler_links table.
  212.     db_query("INSERT INTO {crawler_links} (id, path, crawler_id, status) VALUES ('','%s','','')", url('admin', array('absolute' => TRUE)));
  213.     return true;
  214.   }
  215.  
  216.   /**
  217.    *  This function will extract headers and return the lenght.
  218.    */  
  219.  function drupal_security_scanner_curl_headers($ch = NULL, $header = NULL) {
  220.     static $headers = array();
  221.     if (!isset($ch)) {
  222.       return $headers;
  223.     }
  224.     if(!strncmp($header, "Set-Cookie:", 11)) {
  225.       //  get the cookie
  226.       $cookiestr = trim(substr($header, 11, -1));
  227.       $cookie = explode(';', $cookiestr);
  228.       variable_set('security_scanner_cookie', $cookie[0]);
  229.     }
  230.     return strlen($header);  
  231.   }
  232.  
  233.   /**
  234.    *  Implementation of hook _perm()
  235.    */  
  236.   function security_scanner_perm() {
  237.     return array('access scanner');
  238.   }
  239.  
  240.   /**
  241.    *  Implementation of hook _help()
  242.    */
  243.    function security_scanner_help($path, $arg) {
  244.      switch ($path) {
  245.        case 'security_scanner':
  246.        // Here is some help text for a custom page.
  247.          return t('This sentence contains all the letters in the English alphabet.');
  248.      }
  249.    }
  250. ?>